From 8a0d3158a059c4a8d4aa36a10500697d03f787b6 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 2 Apr 2026 14:39:48 -1000 Subject: [PATCH 01/31] Add host platform benchmarks for number, select, and switch components --- .../benchmarks/components/number/__init__.py | 5 + .../components/number/bench_number.cpp | 121 +++++++++++++ .../components/number/benchmark.yaml | 1 + .../benchmarks/components/select/__init__.py | 5 + .../components/select/bench_select.cpp | 162 ++++++++++++++++++ .../components/select/benchmark.yaml | 1 + .../benchmarks/components/switch/__init__.py | 5 + .../components/switch/bench_switch.cpp | 137 +++++++++++++++ .../components/switch/benchmark.yaml | 1 + 9 files changed, 438 insertions(+) create mode 100644 tests/benchmarks/components/number/__init__.py create mode 100644 tests/benchmarks/components/number/bench_number.cpp create mode 100644 tests/benchmarks/components/number/benchmark.yaml create mode 100644 tests/benchmarks/components/select/__init__.py create mode 100644 tests/benchmarks/components/select/bench_select.cpp create mode 100644 tests/benchmarks/components/select/benchmark.yaml create mode 100644 tests/benchmarks/components/switch/__init__.py create mode 100644 tests/benchmarks/components/switch/bench_switch.cpp create mode 100644 tests/benchmarks/components/switch/benchmark.yaml diff --git a/tests/benchmarks/components/number/__init__.py b/tests/benchmarks/components/number/__init__.py new file mode 100644 index 0000000000..b08f67a095 --- /dev/null +++ b/tests/benchmarks/components/number/__init__.py @@ -0,0 +1,5 @@ +from tests.testing_helpers import ComponentManifestOverride + + +def override_manifest(manifest: ComponentManifestOverride) -> None: + manifest.enable_codegen() diff --git a/tests/benchmarks/components/number/bench_number.cpp b/tests/benchmarks/components/number/bench_number.cpp new file mode 100644 index 0000000000..cae0285124 --- /dev/null +++ b/tests/benchmarks/components/number/bench_number.cpp @@ -0,0 +1,121 @@ +#include + +#include "esphome/components/number/number.h" + +namespace esphome::benchmarks { + +// Inner iteration count to amortize CodSpeed instrumentation overhead. +static constexpr int kInnerIterations = 2000; + +// Minimal Number for benchmarking — control() is a no-op. +class BenchNumber : public number::Number { + public: + void configure(const char *name) { this->configure_entity_(name, 0x12345678, 0); } + + protected: + void control(float value) override { this->publish_state(value); } +}; + +// Helper to create a typical number entity for benchmarks. +static void setup_number(BenchNumber &number) { + number.configure("test_number"); + number.traits.set_min_value(0.0f); + number.traits.set_max_value(100.0f); + number.traits.set_step(1.0f); + number.traits.set_mode(number::NUMBER_MODE_SLIDER); +} + +// --- Number::publish_state() --- +// Measures the publish path: set_has_state, store value, callback dispatch. + +static void NumberPublish_State(benchmark::State &state) { + BenchNumber number; + setup_number(number); + + for (auto _ : state) { + for (int i = 0; i < kInnerIterations; i++) { + number.publish_state(static_cast(i % 100)); + } + benchmark::DoNotOptimize(number.state); + } + state.SetItemsProcessed(state.iterations() * kInnerIterations); +} +BENCHMARK(NumberPublish_State); + +// --- Number::publish_state() with callback --- +// Measures callback dispatch overhead. + +static void NumberPublish_WithCallback(benchmark::State &state) { + BenchNumber number; + setup_number(number); + + uint64_t callback_count = 0; + number.add_on_state_callback([&callback_count](float) { callback_count++; }); + + for (auto _ : state) { + for (int i = 0; i < kInnerIterations; i++) { + number.publish_state(static_cast(i % 100)); + } + benchmark::DoNotOptimize(callback_count); + } + state.SetItemsProcessed(state.iterations() * kInnerIterations); +} +BENCHMARK(NumberPublish_WithCallback); + +// --- NumberCall::perform() set value --- +// The most common number call — setting an absolute value. +// Exercises: validation against min/max, control() dispatch. + +static void NumberCall_SetValue(benchmark::State &state) { + BenchNumber number; + setup_number(number); + number.publish_state(50.0f); + + for (auto _ : state) { + for (int i = 0; i < kInnerIterations; i++) { + float val = static_cast(i % 100); + number.make_call().set_value(val).perform(); + } + benchmark::DoNotOptimize(number.state); + } + state.SetItemsProcessed(state.iterations() * kInnerIterations); +} +BENCHMARK(NumberCall_SetValue); + +// --- NumberCall::perform() increment --- +// Exercises: state read, step arithmetic, max clamping. + +static void NumberCall_Increment(benchmark::State &state) { + BenchNumber number; + setup_number(number); + number.publish_state(0.0f); + + for (auto _ : state) { + for (int i = 0; i < kInnerIterations; i++) { + number.make_call().number_increment(true).perform(); + } + benchmark::DoNotOptimize(number.state); + } + state.SetItemsProcessed(state.iterations() * kInnerIterations); +} +BENCHMARK(NumberCall_Increment); + +// --- NumberCall::perform() decrement --- +// Exercises: state read, step arithmetic, min clamping. + +static void NumberCall_Decrement(benchmark::State &state) { + BenchNumber number; + setup_number(number); + number.publish_state(100.0f); + + for (auto _ : state) { + for (int i = 0; i < kInnerIterations; i++) { + number.make_call().number_decrement(true).perform(); + } + benchmark::DoNotOptimize(number.state); + } + state.SetItemsProcessed(state.iterations() * kInnerIterations); +} +BENCHMARK(NumberCall_Decrement); + +} // namespace esphome::benchmarks diff --git a/tests/benchmarks/components/number/benchmark.yaml b/tests/benchmarks/components/number/benchmark.yaml new file mode 100644 index 0000000000..f435661270 --- /dev/null +++ b/tests/benchmarks/components/number/benchmark.yaml @@ -0,0 +1 @@ +number: diff --git a/tests/benchmarks/components/select/__init__.py b/tests/benchmarks/components/select/__init__.py new file mode 100644 index 0000000000..b08f67a095 --- /dev/null +++ b/tests/benchmarks/components/select/__init__.py @@ -0,0 +1,5 @@ +from tests.testing_helpers import ComponentManifestOverride + + +def override_manifest(manifest: ComponentManifestOverride) -> None: + manifest.enable_codegen() diff --git a/tests/benchmarks/components/select/bench_select.cpp b/tests/benchmarks/components/select/bench_select.cpp new file mode 100644 index 0000000000..9a49a8555d --- /dev/null +++ b/tests/benchmarks/components/select/bench_select.cpp @@ -0,0 +1,162 @@ +#include + +#include "esphome/components/select/select.h" + +namespace esphome::benchmarks { + +// Inner iteration count to amortize CodSpeed instrumentation overhead. +static constexpr int kInnerIterations = 2000; + +// Minimal Select for benchmarking — control() publishes directly by index. +class BenchSelect : public select::Select { + public: + void configure(const char *name) { this->configure_entity_(name, 0x12345678, 0); } + + protected: + void control(size_t index) override { this->publish_state(index); } +}; + +// Option lists matching real Apollo R-PRO-1 usage patterns. +static constexpr const char *kSmallOptions[] = {"off", "on", "auto"}; +static constexpr const char *kLargeOptions[] = {"off", "still", "move", "still+move", "custom1", + "custom2", "custom3", "custom4", "custom5", "custom6"}; + +// Helper to create a select with the given options. +static void setup_select(BenchSelect &select, const char *name, std::initializer_list options) { + select.configure(name); + select.traits.set_options(options); + select.publish_state(size_t(0)); +} + +// --- Select::publish_state(size_t) --- +// The fast path: publish by index, no string lookup. + +static void SelectPublish_ByIndex(benchmark::State &state) { + BenchSelect select; + setup_select(select, "test_select", {"off", "still", "move", "still+move"}); + + for (auto _ : state) { + for (int i = 0; i < kInnerIterations; i++) { + select.publish_state(static_cast(i % 4)); + } + benchmark::DoNotOptimize(select.active_index()); + } + state.SetItemsProcessed(state.iterations() * kInnerIterations); +} +BENCHMARK(SelectPublish_ByIndex); + +// --- Select::publish_state(const char *) --- +// The string path: requires index_of() lookup via strncmp. + +static void SelectPublish_ByString(benchmark::State &state) { + BenchSelect select; + setup_select(select, "test_select", {"off", "still", "move", "still+move"}); + + const char *options[] = {"off", "still", "move", "still+move"}; + + for (auto _ : state) { + for (int i = 0; i < kInnerIterations; i++) { + select.publish_state(options[i % 4]); + } + benchmark::DoNotOptimize(select.active_index()); + } + state.SetItemsProcessed(state.iterations() * kInnerIterations); +} +BENCHMARK(SelectPublish_ByString); + +// --- Select::publish_state() with callback --- +// Measures callback dispatch overhead on the index path. + +static void SelectPublish_WithCallback(benchmark::State &state) { + BenchSelect select; + setup_select(select, "test_select", {"off", "still", "move", "still+move"}); + + uint64_t callback_count = 0; + select.add_on_state_callback([&callback_count](size_t) { callback_count++; }); + + for (auto _ : state) { + for (int i = 0; i < kInnerIterations; i++) { + select.publish_state(static_cast(i % 4)); + } + benchmark::DoNotOptimize(callback_count); + } + state.SetItemsProcessed(state.iterations() * kInnerIterations); +} +BENCHMARK(SelectPublish_WithCallback); + +// --- SelectCall::perform() set by index --- +// The fast call path — no string matching needed. + +static void SelectCall_SetByIndex(benchmark::State &state) { + BenchSelect select; + setup_select(select, "test_select", {"off", "still", "move", "still+move"}); + + for (auto _ : state) { + for (int i = 0; i < kInnerIterations; i++) { + select.make_call().set_index(i % 4).perform(); + } + benchmark::DoNotOptimize(select.active_index()); + } + state.SetItemsProcessed(state.iterations() * kInnerIterations); +} +BENCHMARK(SelectCall_SetByIndex); + +// --- SelectCall::perform() set by option string --- +// Exercises the string lookup path through index_of(). + +static void SelectCall_SetByOption(benchmark::State &state) { + BenchSelect select; + setup_select(select, "test_select", {"off", "still", "move", "still+move"}); + + const char *options[] = {"off", "still", "move", "still+move"}; + + for (auto _ : state) { + for (int i = 0; i < kInnerIterations; i++) { + select.make_call().set_option(options[i % 4]).perform(); + } + benchmark::DoNotOptimize(select.active_index()); + } + state.SetItemsProcessed(state.iterations() * kInnerIterations); +} +BENCHMARK(SelectCall_SetByOption); + +// --- SelectCall::perform() next with cycling --- +// Exercises the navigation path through active_index_. + +static void SelectCall_NextCycle(benchmark::State &state) { + BenchSelect select; + setup_select(select, "test_select", {"off", "still", "move", "still+move"}); + + for (auto _ : state) { + for (int i = 0; i < kInnerIterations; i++) { + select.make_call().select_next(true).perform(); + } + benchmark::DoNotOptimize(select.active_index()); + } + state.SetItemsProcessed(state.iterations() * kInnerIterations); +} +BENCHMARK(SelectCall_NextCycle); + +// --- SelectCall with 10 options (string lookup) --- +// Worst-case string matching with more options. + +static void SelectCall_SetByOption_10Options(benchmark::State &state) { + BenchSelect select; + setup_select( + select, "test_select", + {"off", "still", "move", "still+move", "custom1", "custom2", "custom3", "custom4", "custom5", "custom6"}); + + // Pick options spread across the list to exercise different search depths + const char *picks[] = {"off", "custom3", "custom6", "move"}; + + for (auto _ : state) { + for (int i = 0; i < kInnerIterations; i++) { + select.make_call().set_option(picks[i % 4]).perform(); + } + benchmark::DoNotOptimize(select.active_index()); + } + state.SetItemsProcessed(state.iterations() * kInnerIterations); +} +BENCHMARK(SelectCall_SetByOption_10Options); + +} // namespace esphome::benchmarks diff --git a/tests/benchmarks/components/select/benchmark.yaml b/tests/benchmarks/components/select/benchmark.yaml new file mode 100644 index 0000000000..d336a348a0 --- /dev/null +++ b/tests/benchmarks/components/select/benchmark.yaml @@ -0,0 +1 @@ +select: diff --git a/tests/benchmarks/components/switch/__init__.py b/tests/benchmarks/components/switch/__init__.py new file mode 100644 index 0000000000..b08f67a095 --- /dev/null +++ b/tests/benchmarks/components/switch/__init__.py @@ -0,0 +1,5 @@ +from tests.testing_helpers import ComponentManifestOverride + + +def override_manifest(manifest: ComponentManifestOverride) -> None: + manifest.enable_codegen() diff --git a/tests/benchmarks/components/switch/bench_switch.cpp b/tests/benchmarks/components/switch/bench_switch.cpp new file mode 100644 index 0000000000..d948f080ad --- /dev/null +++ b/tests/benchmarks/components/switch/bench_switch.cpp @@ -0,0 +1,137 @@ +#include + +#include "esphome/components/switch/switch.h" + +namespace esphome::benchmarks { + +// Inner iteration count to amortize CodSpeed instrumentation overhead. +static constexpr int kInnerIterations = 2000; + +// Minimal Switch for benchmarking — write_state() publishes directly. +class BenchSwitch : public switch_::Switch { + public: + void configure(const char *name) { this->configure_entity_(name, 0x12345678, 0); } + + protected: + void write_state(bool state) override { this->publish_state(state); } +}; + +// --- Switch::publish_state() alternating --- +// Forces state change every call, exercising the full publish path. + +static void SwitchPublish_Alternating(benchmark::State &state) { + BenchSwitch sw; + sw.configure("test_switch"); + sw.set_restore_mode(switch_::SWITCH_ALWAYS_OFF); + sw.publish_state(false); + + for (auto _ : state) { + for (int i = 0; i < kInnerIterations; i++) { + sw.publish_state(i % 2 == 0); + } + benchmark::DoNotOptimize(sw.state); + } + state.SetItemsProcessed(state.iterations() * kInnerIterations); +} +BENCHMARK(SwitchPublish_Alternating); + +// --- Switch::publish_state() no change --- +// Tests the deduplication fast path in publish_dedup_. + +static void SwitchPublish_NoChange(benchmark::State &state) { + BenchSwitch sw; + sw.configure("test_switch"); + sw.set_restore_mode(switch_::SWITCH_ALWAYS_OFF); + sw.publish_state(true); + + for (auto _ : state) { + for (int i = 0; i < kInnerIterations; i++) { + sw.publish_state(true); + } + benchmark::DoNotOptimize(sw.state); + } + state.SetItemsProcessed(state.iterations() * kInnerIterations); +} +BENCHMARK(SwitchPublish_NoChange); + +// --- Switch::publish_state() with callback --- +// Measures callback dispatch overhead on state changes. + +static void SwitchPublish_WithCallback(benchmark::State &state) { + BenchSwitch sw; + sw.configure("test_switch"); + sw.set_restore_mode(switch_::SWITCH_ALWAYS_OFF); + + uint64_t callback_count = 0; + sw.add_on_state_callback([&callback_count](bool) { callback_count++; }); + sw.publish_state(false); + + for (auto _ : state) { + for (int i = 0; i < kInnerIterations; i++) { + sw.publish_state(i % 2 == 0); + } + benchmark::DoNotOptimize(callback_count); + } + state.SetItemsProcessed(state.iterations() * kInnerIterations); +} +BENCHMARK(SwitchPublish_WithCallback); + +// --- Switch::turn_on() / turn_off() --- +// The front-end call path: turn_on → write_state → publish_state. + +static void SwitchTurnOn(benchmark::State &state) { + BenchSwitch sw; + sw.configure("test_switch"); + sw.set_restore_mode(switch_::SWITCH_ALWAYS_OFF); + sw.publish_state(false); + + for (auto _ : state) { + for (int i = 0; i < kInnerIterations; i++) { + sw.turn_on(); + } + benchmark::DoNotOptimize(sw.state); + } + state.SetItemsProcessed(state.iterations() * kInnerIterations); +} +BENCHMARK(SwitchTurnOn); + +// --- Switch::toggle() alternating --- +// Exercises the toggle path which reads current state to determine target. + +static void SwitchToggle(benchmark::State &state) { + BenchSwitch sw; + sw.configure("test_switch"); + sw.set_restore_mode(switch_::SWITCH_ALWAYS_OFF); + sw.publish_state(false); + + for (auto _ : state) { + for (int i = 0; i < kInnerIterations; i++) { + sw.toggle(); + } + benchmark::DoNotOptimize(sw.state); + } + state.SetItemsProcessed(state.iterations() * kInnerIterations); +} +BENCHMARK(SwitchToggle); + +// --- Switch::publish_state() inverted --- +// Verifies the inversion path doesn't add significant overhead. + +static void SwitchPublish_Inverted(benchmark::State &state) { + BenchSwitch sw; + sw.configure("test_switch"); + sw.set_restore_mode(switch_::SWITCH_ALWAYS_OFF); + sw.set_inverted(true); + sw.publish_state(false); + + for (auto _ : state) { + for (int i = 0; i < kInnerIterations; i++) { + sw.publish_state(i % 2 == 0); + } + benchmark::DoNotOptimize(sw.state); + } + state.SetItemsProcessed(state.iterations() * kInnerIterations); +} +BENCHMARK(SwitchPublish_Inverted); + +} // namespace esphome::benchmarks diff --git a/tests/benchmarks/components/switch/benchmark.yaml b/tests/benchmarks/components/switch/benchmark.yaml new file mode 100644 index 0000000000..c637b3dc89 --- /dev/null +++ b/tests/benchmarks/components/switch/benchmark.yaml @@ -0,0 +1 @@ +switch: From 46f8e46548b3ecf8e9751d15fc52b0c8b02420b3 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 2 Apr 2026 14:48:43 -1000 Subject: [PATCH 02/31] Downgrade select and switch control path logging from DEBUG to VERBOSE The ESP_LOGD calls in SelectCall::perform() and Switch::turn_on/turn_off/toggle are redundant with aioesphomeapi's state_log_formatter which already logs both commands and state changes on the HA side. Benchmarks show these ESP_LOGD calls dominate the control path cost (~13M ops/s for SelectCall vs ~180M ops/s for publish_state). This aligns select and switch with climate and fan which already use ESP_LOGV for their call perform() paths. --- esphome/components/select/select_call.cpp | 2 +- esphome/components/switch/switch.cpp | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/esphome/components/select/select_call.cpp b/esphome/components/select/select_call.cpp index 0e14371d00..9d2fb725f3 100644 --- a/esphome/components/select/select_call.cpp +++ b/esphome/components/select/select_call.cpp @@ -116,7 +116,7 @@ void SelectCall::perform() { auto idx = target_index.value(); // All operations use indices, call control() by index to avoid string conversion - ESP_LOGD(TAG, "'%s' - Set selected option to: %s", name, parent->option_at(idx)); + ESP_LOGV(TAG, "'%s' - Set selected option to: %s", name, parent->option_at(idx)); parent->control(idx); } diff --git a/esphome/components/switch/switch.cpp b/esphome/components/switch/switch.cpp index 11840db3a3..abc7338a62 100644 --- a/esphome/components/switch/switch.cpp +++ b/esphome/components/switch/switch.cpp @@ -18,15 +18,15 @@ void Switch::control(bool target_state) { } } void Switch::turn_on() { - ESP_LOGD(TAG, "'%s' Turning ON.", this->get_name().c_str()); + ESP_LOGV(TAG, "'%s' Turning ON.", this->get_name().c_str()); this->write_state(!this->inverted_); } void Switch::turn_off() { - ESP_LOGD(TAG, "'%s' Turning OFF.", this->get_name().c_str()); + ESP_LOGV(TAG, "'%s' Turning OFF.", this->get_name().c_str()); this->write_state(this->inverted_); } void Switch::toggle() { - ESP_LOGD(TAG, "'%s' Toggling %s.", this->get_name().c_str(), this->state ? "OFF" : "ON"); + ESP_LOGV(TAG, "'%s' Toggling %s.", this->get_name().c_str(), this->state ? "OFF" : "ON"); this->write_state(this->inverted_ == this->state); } optional Switch::get_initial_state() { From 2058eda7fa62e1b1bd74110e244164dd7c9dfa4e Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 2 Apr 2026 14:54:28 -1000 Subject: [PATCH 03/31] Address review: fix misleading comment, remove unused constants --- tests/benchmarks/components/number/bench_number.cpp | 2 +- tests/benchmarks/components/select/bench_select.cpp | 5 ----- 2 files changed, 1 insertion(+), 6 deletions(-) diff --git a/tests/benchmarks/components/number/bench_number.cpp b/tests/benchmarks/components/number/bench_number.cpp index cae0285124..57a73930b2 100644 --- a/tests/benchmarks/components/number/bench_number.cpp +++ b/tests/benchmarks/components/number/bench_number.cpp @@ -7,7 +7,7 @@ namespace esphome::benchmarks { // Inner iteration count to amortize CodSpeed instrumentation overhead. static constexpr int kInnerIterations = 2000; -// Minimal Number for benchmarking — control() is a no-op. +// Minimal Number for benchmarking — control() publishes the value back. class BenchNumber : public number::Number { public: void configure(const char *name) { this->configure_entity_(name, 0x12345678, 0); } diff --git a/tests/benchmarks/components/select/bench_select.cpp b/tests/benchmarks/components/select/bench_select.cpp index 9a49a8555d..8e047d9151 100644 --- a/tests/benchmarks/components/select/bench_select.cpp +++ b/tests/benchmarks/components/select/bench_select.cpp @@ -16,11 +16,6 @@ class BenchSelect : public select::Select { void control(size_t index) override { this->publish_state(index); } }; -// Option lists matching real Apollo R-PRO-1 usage patterns. -static constexpr const char *kSmallOptions[] = {"off", "on", "auto"}; -static constexpr const char *kLargeOptions[] = {"off", "still", "move", "still+move", "custom1", - "custom2", "custom3", "custom4", "custom5", "custom6"}; - // Helper to create a select with the given options. static void setup_select(BenchSelect &select, const char *name, std::initializer_list options) { select.configure(name); From 27522d7462f31d4eb7d20cf6a5f856af70b12597 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 2 Apr 2026 14:57:39 -1000 Subject: [PATCH 04/31] Add host platform benchmarks for text_sensor and button components --- .../benchmarks/components/button/__init__.py | 5 + .../components/button/bench_button.cpp | 55 +++++++++ .../components/button/benchmark.yaml | 1 + .../components/text_sensor/__init__.py | 5 + .../text_sensor/bench_text_sensor.cpp | 108 ++++++++++++++++++ .../components/text_sensor/benchmark.yaml | 1 + 6 files changed, 175 insertions(+) create mode 100644 tests/benchmarks/components/button/__init__.py create mode 100644 tests/benchmarks/components/button/bench_button.cpp create mode 100644 tests/benchmarks/components/button/benchmark.yaml create mode 100644 tests/benchmarks/components/text_sensor/__init__.py create mode 100644 tests/benchmarks/components/text_sensor/bench_text_sensor.cpp create mode 100644 tests/benchmarks/components/text_sensor/benchmark.yaml diff --git a/tests/benchmarks/components/button/__init__.py b/tests/benchmarks/components/button/__init__.py new file mode 100644 index 0000000000..b08f67a095 --- /dev/null +++ b/tests/benchmarks/components/button/__init__.py @@ -0,0 +1,5 @@ +from tests.testing_helpers import ComponentManifestOverride + + +def override_manifest(manifest: ComponentManifestOverride) -> None: + manifest.enable_codegen() diff --git a/tests/benchmarks/components/button/bench_button.cpp b/tests/benchmarks/components/button/bench_button.cpp new file mode 100644 index 0000000000..82f76961c9 --- /dev/null +++ b/tests/benchmarks/components/button/bench_button.cpp @@ -0,0 +1,55 @@ +#include + +#include "esphome/components/button/button.h" + +namespace esphome::button::benchmarks { + +static constexpr int kInnerIterations = 2000; + +// Minimal Button for benchmarking — press_action() is a no-op. +class BenchButton : public Button { + public: + void configure(const char *name) { this->configure_entity_(name, 0x12345678, 0); } + + protected: + void press_action() override {} +}; + +// --- Button::press() --- +// Measures: ESP_LOGD + press_action() + callback dispatch. + +static void ButtonPress(benchmark::State &state) { + BenchButton button; + button.configure("test_button"); + + for (auto _ : state) { + for (int i = 0; i < kInnerIterations; i++) { + button.press(); + } + benchmark::DoNotOptimize(&button); + } + state.SetItemsProcessed(state.iterations() * kInnerIterations); +} +BENCHMARK(ButtonPress); + +// --- Button::press() with callback --- +// Measures callback dispatch overhead. + +static void ButtonPress_WithCallback(benchmark::State &state) { + BenchButton button; + button.configure("test_button"); + + uint64_t callback_count = 0; + button.add_on_press_callback([&callback_count]() { callback_count++; }); + + for (auto _ : state) { + for (int i = 0; i < kInnerIterations; i++) { + button.press(); + } + benchmark::DoNotOptimize(callback_count); + } + state.SetItemsProcessed(state.iterations() * kInnerIterations); +} +BENCHMARK(ButtonPress_WithCallback); + +} // namespace esphome::button::benchmarks diff --git a/tests/benchmarks/components/button/benchmark.yaml b/tests/benchmarks/components/button/benchmark.yaml new file mode 100644 index 0000000000..75f089f793 --- /dev/null +++ b/tests/benchmarks/components/button/benchmark.yaml @@ -0,0 +1 @@ +button: diff --git a/tests/benchmarks/components/text_sensor/__init__.py b/tests/benchmarks/components/text_sensor/__init__.py new file mode 100644 index 0000000000..b08f67a095 --- /dev/null +++ b/tests/benchmarks/components/text_sensor/__init__.py @@ -0,0 +1,5 @@ +from tests.testing_helpers import ComponentManifestOverride + + +def override_manifest(manifest: ComponentManifestOverride) -> None: + manifest.enable_codegen() diff --git a/tests/benchmarks/components/text_sensor/bench_text_sensor.cpp b/tests/benchmarks/components/text_sensor/bench_text_sensor.cpp new file mode 100644 index 0000000000..0ac88f79c1 --- /dev/null +++ b/tests/benchmarks/components/text_sensor/bench_text_sensor.cpp @@ -0,0 +1,108 @@ +#include + +#include "esphome/components/text_sensor/text_sensor.h" + +namespace esphome::text_sensor::benchmarks { + +static constexpr int kInnerIterations = 2000; + +// --- publish_state(const char *) with short string, value changes each time --- +// Exercises: memcmp check (mismatch), string assign, callback dispatch. + +static void TextSensorPublish_Short_Changing(benchmark::State &state) { + TextSensor sensor; + + // Pre-populate with different short strings + const char *values[] = {"192.168.1.1", "192.168.1.2", "192.168.1.3", "192.168.1.4"}; + + for (auto _ : state) { + for (int i = 0; i < kInnerIterations; i++) { + sensor.publish_state(values[i % 4]); + } + benchmark::DoNotOptimize(sensor.state); + } + state.SetItemsProcessed(state.iterations() * kInnerIterations); +} +BENCHMARK(TextSensorPublish_Short_Changing); + +// --- publish_state(const char *) with short string, same value (dedup path) --- +// Exercises: memcmp check (match), skips string assign. + +static void TextSensorPublish_Short_NoChange(benchmark::State &state) { + TextSensor sensor; + sensor.publish_state("192.168.1.100"); + + for (auto _ : state) { + for (int i = 0; i < kInnerIterations; i++) { + sensor.publish_state("192.168.1.100"); + } + benchmark::DoNotOptimize(sensor.state); + } + state.SetItemsProcessed(state.iterations() * kInnerIterations); +} +BENCHMARK(TextSensorPublish_Short_NoChange); + +// --- publish_state with longer string (firmware version, MAC address) --- +// Exercises: memcmp on longer strings, string assign with potential realloc. + +static void TextSensorPublish_Long_Changing(benchmark::State &state) { + TextSensor sensor; + + const char *values[] = { + "2025.12.0-dev (Jan 15 2025, 10:30:00)", + "2025.12.1-dev (Feb 20 2025, 14:45:00)", + "2025.12.2-dev (Mar 10 2025, 08:15:00)", + "2025.12.3-dev (Apr 5 2025, 16:00:00)", + }; + + for (auto _ : state) { + for (int i = 0; i < kInnerIterations; i++) { + sensor.publish_state(values[i % 4]); + } + benchmark::DoNotOptimize(sensor.state); + } + state.SetItemsProcessed(state.iterations() * kInnerIterations); +} +BENCHMARK(TextSensorPublish_Long_Changing); + +// --- publish_state with callback --- +// Measures callback dispatch overhead for text sensors. + +static void TextSensorPublish_WithCallback(benchmark::State &state) { + TextSensor sensor; + + uint64_t callback_count = 0; + sensor.add_on_state_callback([&callback_count](const std::string &) { callback_count++; }); + + const char *values[] = {"192.168.1.1", "192.168.1.2", "192.168.1.3", "192.168.1.4"}; + + for (auto _ : state) { + for (int i = 0; i < kInnerIterations; i++) { + sensor.publish_state(values[i % 4]); + } + benchmark::DoNotOptimize(callback_count); + } + state.SetItemsProcessed(state.iterations() * kInnerIterations); +} +BENCHMARK(TextSensorPublish_WithCallback); + +// --- publish_state(const char *, size_t) direct --- +// The lowest-level overload, avoids strlen. + +static void TextSensorPublish_WithLen(benchmark::State &state) { + TextSensor sensor; + + static constexpr const char *values[] = {"192.168.1.1", "192.168.1.2", "192.168.1.3", "192.168.1.4"}; + static constexpr size_t lens[] = {11, 11, 11, 11}; + + for (auto _ : state) { + for (int i = 0; i < kInnerIterations; i++) { + sensor.publish_state(values[i % 4], lens[i % 4]); + } + benchmark::DoNotOptimize(sensor.state); + } + state.SetItemsProcessed(state.iterations() * kInnerIterations); +} +BENCHMARK(TextSensorPublish_WithLen); + +} // namespace esphome::text_sensor::benchmarks diff --git a/tests/benchmarks/components/text_sensor/benchmark.yaml b/tests/benchmarks/components/text_sensor/benchmark.yaml new file mode 100644 index 0000000000..7bcb7de1c8 --- /dev/null +++ b/tests/benchmarks/components/text_sensor/benchmark.yaml @@ -0,0 +1 @@ +text_sensor: From 59c33628a6e9fa8551ed2ebfa61745339d665e95 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 2 Apr 2026 15:04:14 -1000 Subject: [PATCH 05/31] Downgrade button press logging from DEBUG to VERBOSE --- esphome/components/button/button.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/button/button.cpp b/esphome/components/button/button.cpp index b1c491805e..2a2a645132 100644 --- a/esphome/components/button/button.cpp +++ b/esphome/components/button/button.cpp @@ -16,7 +16,7 @@ void log_button(const char *tag, const char *prefix, const char *type, Button *o } void Button::press() { - ESP_LOGD(TAG, "'%s' Pressed.", this->get_name().c_str()); + ESP_LOGV(TAG, "'%s' Pressed.", this->get_name().c_str()); this->press_action(); this->press_callback_.call(); } From 288d98f5ef034b92d5f63b71cd9faed50f981a08 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 2 Apr 2026 15:11:00 -1000 Subject: [PATCH 06/31] [uptime] Pass known length to publish_state to avoid redundant strlen --- esphome/components/uptime/text_sensor/uptime_text_sensor.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/uptime/text_sensor/uptime_text_sensor.cpp b/esphome/components/uptime/text_sensor/uptime_text_sensor.cpp index c89d23672e..7a56654804 100644 --- a/esphome/components/uptime/text_sensor/uptime_text_sensor.cpp +++ b/esphome/components/uptime/text_sensor/uptime_text_sensor.cpp @@ -70,7 +70,7 @@ void UptimeTextSensor::update() { if (show_seconds) append_unit(buf, sizeof(buf), pos, this->separator_, seconds, this->seconds_text_); - this->publish_state(buf); + this->publish_state(buf, pos); } float UptimeTextSensor::get_setup_priority() const { return setup_priority::HARDWARE; } From d341ee8c767ff8b4bc75a5b16b264ba8be5c6184 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 3 Apr 2026 10:21:20 -1000 Subject: [PATCH 07/31] [esp32_ble] Skip dropped count check when queue is empty On Xtensa (dual-core ESP32), even relaxed atomic loads emit a memw barrier instruction. The dropped_count_ check in get_and_reset_dropped_count() was executing every loop() iteration even when the queue was empty. Since drops only occur when the queue is full, and only the main loop drains the queue, an empty queue guarantees no new drops since the last reset. Restructure the loop to early-return when pop() returns nullptr, skipping the unnecessary memw. Also moves advertising_->loop() before the queue drain so it runs unconditionally without duplication, and converts while to do/while since the first element is known non-null after the nullptr check. --- esphome/components/esp32_ble/ble.cpp | 23 ++++++++++++++--------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/esphome/components/esp32_ble/ble.cpp b/esphome/components/esp32_ble/ble.cpp index 2cd2ec67f7..68e5fffe2b 100644 --- a/esphome/components/esp32_ble/ble.cpp +++ b/esphome/components/esp32_ble/ble.cpp @@ -399,8 +399,17 @@ void ESP32BLE::loop() { return; } +#ifdef USE_ESP32_BLE_ADVERTISING + if (this->advertising_ != nullptr) { + this->advertising_->loop(); + } +#endif + BLEEvent *ble_event = this->ble_events_.pop(); - while (ble_event != nullptr) { + if (ble_event == nullptr) + return; + + do { switch (ble_event->type_) { #if defined(USE_ESP32_BLE_SERVER) && defined(ESPHOME_ESP32_BLE_GATTS_EVENT_HANDLER_COUNT) case BLEEvent::GATTS: { @@ -488,15 +497,11 @@ void ESP32BLE::loop() { } // Return the event to the pool this->ble_event_pool_.release(ble_event); - ble_event = this->ble_events_.pop(); - } -#ifdef USE_ESP32_BLE_ADVERTISING - if (this->advertising_ != nullptr) { - this->advertising_->loop(); - } -#endif + } while ((ble_event = this->ble_events_.pop()) != nullptr); - // Log dropped events periodically + // Log dropped events - only reachable when events were processed. + // Drops only occur when the queue is full, and only this loop drains it, + // so if pop() returned nullptr above we can skip this check (saves a memw). uint16_t dropped = this->ble_events_.get_and_reset_dropped_count(); if (dropped > 0) { ESP_LOGW(TAG, "Dropped %u BLE events due to buffer overflow", dropped); From 669a308209cdeda6fa647f5a4f41611507231005 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 3 Apr 2026 10:23:49 -1000 Subject: [PATCH 08/31] Use pop() instead of empty() for wifi_loop_ fast path Replace empty() + pop() with pop() directly as the fast-path check: - LockFreeQueue: pop() costs 1 memw (acquire on tail_) vs empty()'s 2 memw (acquire on both head_ and tail_) on Xtensa - FreeRTOSQueue: pop() is 1 critical section vs empty() + pop() = 2 Also move dropped count check after the drain loop since drops can only occur when the queue was full, and only this loop drains it. --- .../wifi/wifi_component_esp_idf.cpp | 19 ++++++++++-------- .../wifi/wifi_component_libretiny.cpp | 20 +++++++++++-------- 2 files changed, 23 insertions(+), 16 deletions(-) diff --git a/esphome/components/wifi/wifi_component_esp_idf.cpp b/esphome/components/wifi/wifi_component_esp_idf.cpp index 2fcd0b7a0b..c790742c79 100644 --- a/esphome/components/wifi/wifi_component_esp_idf.cpp +++ b/esphome/components/wifi/wifi_component_esp_idf.cpp @@ -716,20 +716,23 @@ const char *get_disconnect_reason_str(uint8_t reason) { } bool WiFiComponent::wifi_loop_() { - // Fast path: skip dropped count check and pop loop when queue is empty - if (this->event_queue_.empty()) + // Use pop() directly instead of empty() — pop() costs 1 memw (acquire on tail_), + // while empty() costs 2 memw (acquire on both head_ and tail_) on Xtensa. + IDFWiFiEvent *data = this->event_queue_.pop(); + if (data == nullptr) return false; + do { + wifi_process_event_(data); + delete data; // NOLINT(cppcoreguidelines-owning-memory) + } while ((data = this->event_queue_.pop()) != nullptr); + + // Drops only occur when the queue is full, and only this loop drains it, + // so if pop() returned nullptr above we can skip this check. uint16_t dropped = this->event_queue_.get_and_reset_dropped_count(); if (dropped > 0) { ESP_LOGW(TAG, "Dropped %u WiFi events due to buffer overflow", dropped); } - - IDFWiFiEvent *data; - while ((data = this->event_queue_.pop()) != nullptr) { - wifi_process_event_(data); - delete data; // NOLINT(cppcoreguidelines-owning-memory) - } return true; } // Events are processed from queue in main loop context, but listener notifications diff --git a/esphome/components/wifi/wifi_component_libretiny.cpp b/esphome/components/wifi/wifi_component_libretiny.cpp index 7ca3549cab..cdd11ceaef 100644 --- a/esphome/components/wifi/wifi_component_libretiny.cpp +++ b/esphome/components/wifi/wifi_component_libretiny.cpp @@ -778,20 +778,24 @@ network::IPAddress WiFiComponent::wifi_subnet_mask_() { return {WiFi.subnetMask( network::IPAddress WiFiComponent::wifi_gateway_ip_() { return {WiFi.gatewayIP()}; } network::IPAddress WiFiComponent::wifi_dns_ip_(int num) { return {WiFi.dnsIP(num)}; } bool WiFiComponent::wifi_loop_() { - // Fast path: skip dropped count check and pop loop when queue is empty - if (this->event_queue_.empty()) + // Use pop() directly instead of empty() — avoids redundant synchronization. + // LockFreeQueue: pop() costs 1 memw vs empty()'s 2 memw on Xtensa. + // FreeRTOSQueue: pop() is 1 critical section vs empty() + pop() = 2. + LTWiFiEvent *event = this->event_queue_.pop(); + if (event == nullptr) return false; + do { + wifi_process_event_(event); + delete event; // NOLINT(cppcoreguidelines-owning-memory) + } while ((event = this->event_queue_.pop()) != nullptr); + + // Drops only occur when the queue is full, and only this loop drains it, + // so if pop() returned nullptr above we can skip this check. uint16_t dropped = this->event_queue_.get_and_reset_dropped_count(); if (dropped > 0) { ESP_LOGW(TAG, "Dropped %" PRIu16 " WiFi events due to buffer overflow", dropped); } - - LTWiFiEvent *event; - while ((event = this->event_queue_.pop()) != nullptr) { - wifi_process_event_(event); - delete event; // NOLINT(cppcoreguidelines-owning-memory) - } return true; } From e35aa729f3ec6ff6640b3035cb20241766bc8514 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 3 Apr 2026 12:21:37 -1000 Subject: [PATCH 09/31] [api] Add max_value proto option for constant-size varint codegen MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add a max_value field option to api_options.proto that tells the code generator the maximum value a field can have. When max_value < 128, the generated calculate_size() uses constant arithmetic instead of calling varint size functions, and encode() uses direct byte writes instead of varint encoding. Also optimize FixedArrayBytesType: when fixed_array_size < 128, the length varint is always 1 byte, so calculate_size() uses constant arithmetic and encode() uses write_raw_byte for the length. Applied to BluetoothLERawAdvertisement.address_type (max_value=4). Measured on ESP32 (upstairsdesk89proxy): - BluetoothLERawAdvertisement::calculate_size: 88 → 71 bytes (-19%) - BluetoothLERawAdvertisement::encode: 199 → 179 bytes (-10%) - Total BLE proxy hot path: 1807 → 1770 bytes (-37 bytes) --- esphome/components/api/api.proto | 2 +- esphome/components/api/api_options.proto | 6 +++ esphome/components/api/api_pb2.cpp | 6 +-- script/api_protobuf/api_protobuf.py | 47 ++++++++++++++++++++++-- 4 files changed, 54 insertions(+), 7 deletions(-) diff --git a/esphome/components/api/api.proto b/esphome/components/api/api.proto index 96ee2fb920..1e03675999 100644 --- a/esphome/components/api/api.proto +++ b/esphome/components/api/api.proto @@ -1606,7 +1606,7 @@ message BluetoothLEAdvertisementResponse { message BluetoothLERawAdvertisement { uint64 address = 1 [(force) = true]; sint32 rssi = 2 [(force) = true]; - uint32 address_type = 3; + uint32 address_type = 3 [(max_value) = 4]; bytes data = 4 [(fixed_array_size) = 62, (force) = true]; } diff --git a/esphome/components/api/api_options.proto b/esphome/components/api/api_options.proto index 02600f0977..0aa9e814cf 100644 --- a/esphome/components/api/api_options.proto +++ b/esphome/components/api/api_options.proto @@ -96,4 +96,10 @@ extend google.protobuf.FieldOptions { // variant of the calc_ method. Use on fields that are almost always non-default // to eliminate dead branches on hot paths. optional bool force = 50016 [default=false]; + + // max_value: Maximum value a field can have. + // When max_value < 128, the code generator emits constant-size calculations + // and direct byte writes instead of varint branching, since the encoded varint + // is guaranteed to be 1 byte. + optional uint32 max_value = 50017; } diff --git a/esphome/components/api/api_pb2.cpp b/esphome/components/api/api_pb2.cpp index ae2cd2bae8..f25d269e8f 100644 --- a/esphome/components/api/api_pb2.cpp +++ b/esphome/components/api/api_pb2.cpp @@ -2255,15 +2255,15 @@ void BluetoothLERawAdvertisement::encode(ProtoWriteBuffer &buffer) const { buffer.encode_varint_raw(encode_zigzag32(this->rssi)); buffer.encode_uint32(3, this->address_type); buffer.write_raw_byte(34); - buffer.encode_varint_raw(this->data_len); + buffer.write_raw_byte(static_cast(this->data_len)); buffer.encode_raw(this->data, this->data_len); } uint32_t BluetoothLERawAdvertisement::calculate_size() const { uint32_t size = 0; size += ProtoSize::calc_uint64_force(1, this->address); size += ProtoSize::calc_sint32_force(1, this->rssi); - size += ProtoSize::calc_uint32(1, this->address_type); - size += ProtoSize::calc_length_force(1, this->data_len); + size += this->address_type ? 2 : 0; + size += 2 + this->data_len; return size; } void BluetoothLERawAdvertisementsResponse::encode(ProtoWriteBuffer &buffer) const { diff --git a/script/api_protobuf/api_protobuf.py b/script/api_protobuf/api_protobuf.py index f2a11141af..a8870f0f53 100755 --- a/script/api_protobuf/api_protobuf.py +++ b/script/api_protobuf/api_protobuf.py @@ -156,6 +156,11 @@ class TypeInfo(ABC): """Check if this field should always be encoded (skip zero/empty check).""" return get_field_opt(self._field, pb.force, False) + @property + def max_value(self) -> int | None: + """Get the max_value option for this field, or None if not set.""" + return get_field_opt(self._field, pb.max_value, None) + @property def wire_type(self) -> WireType: """Get the wire type for the field.""" @@ -240,32 +245,55 @@ class TypeInfo(ABC): Returns the raw encode string if the tag is a single byte and the encode_func has a known raw equivalent, or None otherwise. + When max_value < 128, uses direct byte write instead of varint encoding. """ if not self.force: return None tag = self.calculate_tag() if tag >= 128: return None + # When max_value < 128, varint is always 1 byte - use direct byte write + max_val = self.max_value + if ( + max_val is not None + and max_val < 128 + and self.encode_func + in ( + "encode_uint32", + "encode_uint64", + ) + ): + return ( + f"buffer.write_raw_byte({tag});\n" + f"buffer.write_raw_byte(static_cast({value_expr}));" + ) raw_expr = self.RAW_ENCODE_MAP.get(self.encode_func) if raw_expr is None: return None return f"buffer.write_raw_byte({tag});\n{raw_expr.format(value=value_expr)}" def _encode_bytes_with_precomputed_tag( - self, data_expr: str, len_expr: str + self, data_expr: str, len_expr: str, max_len: int | None = None ) -> str | None: """Try to emit a precomputed-tag encode for a forced bytes/string field. Returns the raw encode string if the tag is a single byte, or None. + When max_len < 128, uses direct byte write for the length varint. """ if not self.force: return None tag = self.calculate_tag() if tag >= 128: return None + # When max_len < 128, length varint is always 1 byte + len_encode = ( + f"buffer.write_raw_byte(static_cast({len_expr}));" + if max_len is not None and max_len < 128 + else f"buffer.encode_varint_raw({len_expr});" + ) return ( f"buffer.write_raw_byte({tag});\n" - f"buffer.encode_varint_raw({len_expr});\n" + f"{len_encode}\n" f"buffer.encode_raw({data_expr}, {len_expr});" ) @@ -1191,8 +1219,9 @@ class FixedArrayBytesType(TypeInfo): @property def encode_content(self) -> str: + max_len = self.array_size if isinstance(self.array_size, int) else None if result := self._encode_bytes_with_precomputed_tag( - f"this->{self.field_name}", f"this->{self.field_name}_len" + f"this->{self.field_name}", f"this->{self.field_name}_len", max_len=max_len ): return result if self.force: @@ -1214,6 +1243,12 @@ class FixedArrayBytesType(TypeInfo): length_field = f"this->{self.field_name}_len" field_id_size = self.calculate_field_id_size() + # When array_size < 128, length varint is always 1 byte + if isinstance(self.array_size, int) and self.array_size < 128: + if force: + return f"size += {field_id_size + 1} + {length_field};" + return f"size += {length_field} ? {field_id_size + 1} + {length_field} : 0;" + if force: # For repeated fields, always calculate size (no zero check) return f"size += ProtoSize::calc_length_force({field_id_size}, {length_field});" @@ -1245,6 +1280,12 @@ class UInt32Type(TypeInfo): return o def get_size_calculation(self, name: str, force: bool = False) -> str: + max_val = self.max_value + if max_val is not None and max_val < 128: + field_id_size = self.calculate_field_id_size() + if force: + return f"size += {field_id_size + 1};" + return f"size += {name} ? {field_id_size + 1} : 0;" return self._get_simple_size_calculation(name, force, "uint32") def get_estimated_size(self) -> int: From 1e68a90ac9364cdef366aa20940212127692b58c Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 3 Apr 2026 12:26:51 -1000 Subject: [PATCH 10/31] [api] Extract _get_single_byte_varint_size helper in codegen Refactor the constant-size varint pattern into a reusable helper method on the TypeInfo base class, used by both UInt32Type (max_value) and FixedArrayBytesType (fixed_array_size < 128). --- script/api_protobuf/api_protobuf.py | 35 ++++++++++++++++++++++------- 1 file changed, 27 insertions(+), 8 deletions(-) diff --git a/script/api_protobuf/api_protobuf.py b/script/api_protobuf/api_protobuf.py index a8870f0f53..66e3182375 100755 --- a/script/api_protobuf/api_protobuf.py +++ b/script/api_protobuf/api_protobuf.py @@ -374,6 +374,28 @@ class TypeInfo(ABC): value = value_expr or name return f"size += ProtoSize::{method}({field_id_size}, {value});" + def _get_single_byte_varint_size( + self, name: str, force: bool, extra_expr: str | None = None + ) -> str: + """Size calculation when the varint is guaranteed to be 1 byte. + + Used when max_value < 128 or fixed_array_size < 128. + The fixed part is field_id_size + 1 (tag + 1-byte varint). + + Args: + name: Expression to check for zero (non-force only) + force: Whether to skip the zero check + extra_expr: Additional variable expression to add (e.g., data length) + """ + fixed = self.calculate_field_id_size() + 1 + if extra_expr: + if force: + return f"size += {fixed} + {extra_expr};" + return f"size += {name} ? {fixed} + {extra_expr} : 0;" + if force: + return f"size += {fixed};" + return f"size += {name} ? {fixed} : 0;" + @abstractmethod def get_size_calculation(self, name: str, force: bool = False) -> str: """Calculate the size needed for encoding this field. @@ -1241,14 +1263,14 @@ class FixedArrayBytesType(TypeInfo): 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" - field_id_size = self.calculate_field_id_size() # When array_size < 128, length varint is always 1 byte if isinstance(self.array_size, int) and self.array_size < 128: - if force: - return f"size += {field_id_size + 1} + {length_field};" - return f"size += {length_field} ? {field_id_size + 1} + {length_field} : 0;" + return self._get_single_byte_varint_size( + length_field, force, extra_expr=length_field + ) + field_id_size = self.calculate_field_id_size() if force: # For repeated fields, always calculate size (no zero check) return f"size += ProtoSize::calc_length_force({field_id_size}, {length_field});" @@ -1282,10 +1304,7 @@ class UInt32Type(TypeInfo): def get_size_calculation(self, name: str, force: bool = False) -> str: max_val = self.max_value if max_val is not None and max_val < 128: - field_id_size = self.calculate_field_id_size() - if force: - return f"size += {field_id_size + 1};" - return f"size += {name} ? {field_id_size + 1} : 0;" + return self._get_single_byte_varint_size(name, force) return self._get_simple_size_calculation(name, force, "uint32") def get_estimated_size(self) -> int: From 363d811cc1f9818c394f3b5f3b8db74699042dea Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 3 Apr 2026 12:27:55 -1000 Subject: [PATCH 11/31] [api] Use _get_simple_size_calculation for FixedArrayBytesType fallback Replace inline calc_length/calc_length_force formatting with the existing helper method. --- script/api_protobuf/api_protobuf.py | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/script/api_protobuf/api_protobuf.py b/script/api_protobuf/api_protobuf.py index 66e3182375..4012db336e 100755 --- a/script/api_protobuf/api_protobuf.py +++ b/script/api_protobuf/api_protobuf.py @@ -1270,12 +1270,7 @@ class FixedArrayBytesType(TypeInfo): length_field, force, extra_expr=length_field ) - field_id_size = self.calculate_field_id_size() - if force: - # For repeated fields, always calculate size (no zero check) - return f"size += ProtoSize::calc_length_force({field_id_size}, {length_field});" - # For non-repeated fields, length already checks for zero - return f"size += ProtoSize::calc_length({field_id_size}, {length_field});" + return self._get_simple_size_calculation(length_field, force, "length") def get_estimated_size(self) -> int: # Estimate based on typical BLE advertisement size From 02b424388e4ef62207bb1d71ef160ccaf114673c Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 3 Apr 2026 12:28:51 -1000 Subject: [PATCH 12/31] [api] Use RAW_ENCODE_SMALL_MAP for max_value encode optimization Replace inline encode_func check with a lookup table, matching the existing RAW_ENCODE_MAP pattern. --- script/api_protobuf/api_protobuf.py | 26 +++++++++++--------------- 1 file changed, 11 insertions(+), 15 deletions(-) diff --git a/script/api_protobuf/api_protobuf.py b/script/api_protobuf/api_protobuf.py index 4012db336e..1381d5586f 100755 --- a/script/api_protobuf/api_protobuf.py +++ b/script/api_protobuf/api_protobuf.py @@ -240,6 +240,12 @@ class TypeInfo(ABC): "encode_bool": "buffer.write_raw_byte({value} ? 0x01 : 0x00);", } + # When max_value < 128, the varint is always 1 byte — use a direct byte write + RAW_ENCODE_SMALL_MAP: dict[str, str] = { + "encode_uint32": "buffer.write_raw_byte(static_cast({value}));", + "encode_uint64": "buffer.write_raw_byte(static_cast({value}));", + } + def _encode_with_precomputed_tag(self, value_expr: str) -> str | None: """Try to emit a precomputed-tag encode for a forced field. @@ -252,22 +258,12 @@ class TypeInfo(ABC): tag = self.calculate_tag() if tag >= 128: return None - # When max_value < 128, varint is always 1 byte - use direct byte write max_val = self.max_value - if ( - max_val is not None - and max_val < 128 - and self.encode_func - in ( - "encode_uint32", - "encode_uint64", - ) - ): - return ( - f"buffer.write_raw_byte({tag});\n" - f"buffer.write_raw_byte(static_cast({value_expr}));" - ) - raw_expr = self.RAW_ENCODE_MAP.get(self.encode_func) + raw_expr = ( + self.RAW_ENCODE_SMALL_MAP.get(self.encode_func) + if max_val is not None and max_val < 128 + else self.RAW_ENCODE_MAP.get(self.encode_func) + ) if raw_expr is None: return None return f"buffer.write_raw_byte({tag});\n{raw_expr.format(value=value_expr)}" From f3238a857e2d2d356fd387ffa20ac158ed747091 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 3 Apr 2026 12:31:01 -1000 Subject: [PATCH 13/31] [api] Select encode map before lookup --- script/api_protobuf/api_protobuf.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/script/api_protobuf/api_protobuf.py b/script/api_protobuf/api_protobuf.py index 1381d5586f..83aee17cf9 100755 --- a/script/api_protobuf/api_protobuf.py +++ b/script/api_protobuf/api_protobuf.py @@ -259,11 +259,12 @@ class TypeInfo(ABC): if tag >= 128: return None max_val = self.max_value - raw_expr = ( - self.RAW_ENCODE_SMALL_MAP.get(self.encode_func) + encode_map = ( + self.RAW_ENCODE_SMALL_MAP if max_val is not None and max_val < 128 - else self.RAW_ENCODE_MAP.get(self.encode_func) + else self.RAW_ENCODE_MAP ) + raw_expr = encode_map.get(self.encode_func) if raw_expr is None: return None return f"buffer.write_raw_byte({tag});\n{raw_expr.format(value=value_expr)}" From fa80caf0ffc3a9d252ac8e789dbfbb4ca7817362 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 3 Apr 2026 12:31:44 -1000 Subject: [PATCH 14/31] [api] Fall back to RAW_ENCODE_MAP when SMALL_MAP has no entry --- script/api_protobuf/api_protobuf.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/script/api_protobuf/api_protobuf.py b/script/api_protobuf/api_protobuf.py index 83aee17cf9..cef4067ea3 100755 --- a/script/api_protobuf/api_protobuf.py +++ b/script/api_protobuf/api_protobuf.py @@ -259,12 +259,11 @@ class TypeInfo(ABC): if tag >= 128: return None max_val = self.max_value - encode_map = ( - self.RAW_ENCODE_SMALL_MAP - if max_val is not None and max_val < 128 - else self.RAW_ENCODE_MAP - ) - raw_expr = encode_map.get(self.encode_func) + raw_expr = None + if max_val is not None and max_val < 128: + raw_expr = self.RAW_ENCODE_SMALL_MAP.get(self.encode_func) + if raw_expr is None: + raw_expr = self.RAW_ENCODE_MAP.get(self.encode_func) if raw_expr is None: return None return f"buffer.write_raw_byte({tag});\n{raw_expr.format(value=value_expr)}" From 9ece286a26f8c3aaa10a8e7025934866e589efc7 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 3 Apr 2026 12:32:32 -1000 Subject: [PATCH 15/31] [api] Simplify _get_single_byte_varint_size --- script/api_protobuf/api_protobuf.py | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/script/api_protobuf/api_protobuf.py b/script/api_protobuf/api_protobuf.py index cef4067ea3..c17f16412c 100755 --- a/script/api_protobuf/api_protobuf.py +++ b/script/api_protobuf/api_protobuf.py @@ -384,13 +384,10 @@ class TypeInfo(ABC): extra_expr: Additional variable expression to add (e.g., data length) """ fixed = self.calculate_field_id_size() + 1 - if extra_expr: - if force: - return f"size += {fixed} + {extra_expr};" - return f"size += {name} ? {fixed} + {extra_expr} : 0;" + size_expr = f"{fixed} + {extra_expr}" if extra_expr else str(fixed) if force: - return f"size += {fixed};" - return f"size += {name} ? {fixed} : 0;" + return f"size += {size_expr};" + return f"size += {name} ? {size_expr} : 0;" @abstractmethod def get_size_calculation(self, name: str, force: bool = False) -> str: From e174e579ed3a3534edc2158eb59ac9a0d7fc4be0 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 3 Apr 2026 13:33:51 -1000 Subject: [PATCH 16/31] [api] Add max_data_length proto option and optimize entity name/object_id Add max_data_length field option for string/bytes fields. When max_data_length < 128, the codegen emits constant-size length varint calculations and direct byte writes. Annotate all entity name and object_id fields with (max_data_length) = 120 and (force) = true across all 25 ListEntities*Response messages (50 fields). Generated code changes: - calculate_size: `calc_length(1, size)` -> `2 + size` (constant) - encode: `encode_string(N, ref)` -> `write_raw_byte(tag) + write_raw_byte(len) + encode_raw(data, len)` Eliminates 2 function calls per field per entity list response, removes zero-check branches, and removes varint size computation. --- esphome/components/api/api.proto | 100 ++++---- esphome/components/api/api_options.proto | 6 + esphome/components/api/api_pb2.cpp | 300 +++++++++++++++-------- script/api_protobuf/api_protobuf.py | 17 +- 4 files changed, 271 insertions(+), 152 deletions(-) diff --git a/esphome/components/api/api.proto b/esphome/components/api/api.proto index 1e03675999..9b62f8e758 100644 --- a/esphome/components/api/api.proto +++ b/esphome/components/api/api.proto @@ -315,9 +315,9 @@ message ListEntitiesBinarySensorResponse { option (source) = SOURCE_SERVER; option (ifdef) = "USE_BINARY_SENSOR"; - string object_id = 1; + string object_id = 1 [(max_data_length) = 120, (force) = true]; fixed32 key = 2 [(force) = true]; - string name = 3; + string name = 3 [(max_data_length) = 120, (force) = true]; reserved 4; // Deprecated: was string unique_id string device_class = 5; @@ -349,9 +349,9 @@ message ListEntitiesCoverResponse { option (source) = SOURCE_SERVER; option (ifdef) = "USE_COVER"; - string object_id = 1; + string object_id = 1 [(max_data_length) = 120, (force) = true]; fixed32 key = 2 [(force) = true]; - string name = 3; + string name = 3 [(max_data_length) = 120, (force) = true]; reserved 4; // Deprecated: was string unique_id bool assumed_state = 5; @@ -433,9 +433,9 @@ message ListEntitiesFanResponse { option (source) = SOURCE_SERVER; option (ifdef) = "USE_FAN"; - string object_id = 1; + string object_id = 1 [(max_data_length) = 120, (force) = true]; fixed32 key = 2 [(force) = true]; - string name = 3; + string name = 3 [(max_data_length) = 120, (force) = true]; reserved 4; // Deprecated: was string unique_id bool supports_oscillation = 5; @@ -521,9 +521,9 @@ message ListEntitiesLightResponse { option (source) = SOURCE_SERVER; option (ifdef) = "USE_LIGHT"; - string object_id = 1; + string object_id = 1 [(max_data_length) = 120, (force) = true]; fixed32 key = 2 [(force) = true]; - string name = 3; + string name = 3 [(max_data_length) = 120, (force) = true]; reserved 4; // Deprecated: was string unique_id repeated ColorMode supported_color_modes = 12 [(container_pointer_no_template) = "light::ColorModeMask"]; @@ -626,9 +626,9 @@ message ListEntitiesSensorResponse { option (source) = SOURCE_SERVER; option (ifdef) = "USE_SENSOR"; - string object_id = 1; + string object_id = 1 [(max_data_length) = 120, (force) = true]; fixed32 key = 2 [(force) = true]; - string name = 3; + string name = 3 [(max_data_length) = 120, (force) = true]; reserved 4; // Deprecated: was string unique_id string icon = 5 [(field_ifdef) = "USE_ENTITY_ICON"]; @@ -666,9 +666,9 @@ message ListEntitiesSwitchResponse { option (source) = SOURCE_SERVER; option (ifdef) = "USE_SWITCH"; - string object_id = 1; + string object_id = 1 [(max_data_length) = 120, (force) = true]; fixed32 key = 2 [(force) = true]; - string name = 3; + string name = 3 [(max_data_length) = 120, (force) = true]; reserved 4; // Deprecated: was string unique_id string icon = 5 [(field_ifdef) = "USE_ENTITY_ICON"]; @@ -708,9 +708,9 @@ message ListEntitiesTextSensorResponse { option (source) = SOURCE_SERVER; option (ifdef) = "USE_TEXT_SENSOR"; - string object_id = 1; + string object_id = 1 [(max_data_length) = 120, (force) = true]; fixed32 key = 2 [(force) = true]; - string name = 3; + string name = 3 [(max_data_length) = 120, (force) = true]; reserved 4; // Deprecated: was string unique_id string icon = 5 [(field_ifdef) = "USE_ENTITY_ICON"]; @@ -971,9 +971,9 @@ message ListEntitiesCameraResponse { option (source) = SOURCE_SERVER; option (ifdef) = "USE_CAMERA"; - string object_id = 1; + string object_id = 1 [(max_data_length) = 120, (force) = true]; fixed32 key = 2 [(force) = true]; - string name = 3; + string name = 3 [(max_data_length) = 120, (force) = true]; reserved 4; // Deprecated: was string unique_id bool disabled_by_default = 5; string icon = 6 [(field_ifdef) = "USE_ENTITY_ICON"]; @@ -1056,9 +1056,9 @@ message ListEntitiesClimateResponse { option (source) = SOURCE_SERVER; option (ifdef) = "USE_CLIMATE"; - string object_id = 1; + string object_id = 1 [(max_data_length) = 120, (force) = true]; fixed32 key = 2 [(force) = true]; - string name = 3; + string name = 3 [(max_data_length) = 120, (force) = true]; reserved 4; // Deprecated: was string unique_id bool supports_current_temperature = 5; // Deprecated: use feature_flags @@ -1167,9 +1167,9 @@ message ListEntitiesWaterHeaterResponse { option (source) = SOURCE_SERVER; option (ifdef) = "USE_WATER_HEATER"; - string object_id = 1; + string object_id = 1 [(max_data_length) = 120, (force) = true]; fixed32 key = 2 [(force) = true]; - string name = 3; + string name = 3 [(max_data_length) = 120, (force) = true]; string icon = 4 [(field_ifdef) = "USE_ENTITY_ICON"]; bool disabled_by_default = 5; EntityCategory entity_category = 6; @@ -1243,9 +1243,9 @@ message ListEntitiesNumberResponse { option (source) = SOURCE_SERVER; option (ifdef) = "USE_NUMBER"; - string object_id = 1; + string object_id = 1 [(max_data_length) = 120, (force) = true]; fixed32 key = 2 [(force) = true]; - string name = 3; + string name = 3 [(max_data_length) = 120, (force) = true]; reserved 4; // Deprecated: was string unique_id string icon = 5 [(field_ifdef) = "USE_ENTITY_ICON"]; @@ -1292,9 +1292,9 @@ message ListEntitiesSelectResponse { option (source) = SOURCE_SERVER; option (ifdef) = "USE_SELECT"; - string object_id = 1; + string object_id = 1 [(max_data_length) = 120, (force) = true]; fixed32 key = 2 [(force) = true]; - string name = 3; + string name = 3 [(max_data_length) = 120, (force) = true]; reserved 4; // Deprecated: was string unique_id string icon = 5 [(field_ifdef) = "USE_ENTITY_ICON"]; @@ -1336,9 +1336,9 @@ message ListEntitiesSirenResponse { option (source) = SOURCE_SERVER; option (ifdef) = "USE_SIREN"; - string object_id = 1; + string object_id = 1 [(max_data_length) = 120, (force) = true]; fixed32 key = 2 [(force) = true]; - string name = 3; + string name = 3 [(max_data_length) = 120, (force) = true]; reserved 4; // Deprecated: was string unique_id string icon = 5 [(field_ifdef) = "USE_ENTITY_ICON"]; @@ -1399,9 +1399,9 @@ message ListEntitiesLockResponse { option (source) = SOURCE_SERVER; option (ifdef) = "USE_LOCK"; - string object_id = 1; + string object_id = 1 [(max_data_length) = 120, (force) = true]; fixed32 key = 2 [(force) = true]; - string name = 3; + string name = 3 [(max_data_length) = 120, (force) = true]; reserved 4; // Deprecated: was string unique_id string icon = 5 [(field_ifdef) = "USE_ENTITY_ICON"]; @@ -1448,9 +1448,9 @@ message ListEntitiesButtonResponse { option (source) = SOURCE_SERVER; option (ifdef) = "USE_BUTTON"; - string object_id = 1; + string object_id = 1 [(max_data_length) = 120, (force) = true]; fixed32 key = 2 [(force) = true]; - string name = 3; + string name = 3 [(max_data_length) = 120, (force) = true]; reserved 4; // Deprecated: was string unique_id string icon = 5 [(field_ifdef) = "USE_ENTITY_ICON"]; @@ -1515,9 +1515,9 @@ message ListEntitiesMediaPlayerResponse { option (source) = SOURCE_SERVER; option (ifdef) = "USE_MEDIA_PLAYER"; - string object_id = 1; + string object_id = 1 [(max_data_length) = 120, (force) = true]; fixed32 key = 2 [(force) = true]; - string name = 3; + string name = 3 [(max_data_length) = 120, (force) = true]; reserved 4; // Deprecated: was string unique_id string icon = 5 [(field_ifdef) = "USE_ENTITY_ICON"]; @@ -2103,9 +2103,9 @@ message ListEntitiesAlarmControlPanelResponse { option (source) = SOURCE_SERVER; option (ifdef) = "USE_ALARM_CONTROL_PANEL"; - string object_id = 1; + string object_id = 1 [(max_data_length) = 120, (force) = true]; fixed32 key = 2 [(force) = true]; - string name = 3; + string name = 3 [(max_data_length) = 120, (force) = true]; reserved 4; // Deprecated: was string unique_id string icon = 5 [(field_ifdef) = "USE_ENTITY_ICON"]; bool disabled_by_default = 6; @@ -2150,9 +2150,9 @@ message ListEntitiesTextResponse { option (source) = SOURCE_SERVER; option (ifdef) = "USE_TEXT"; - string object_id = 1; + string object_id = 1 [(max_data_length) = 120, (force) = true]; fixed32 key = 2 [(force) = true]; - string name = 3; + string name = 3 [(max_data_length) = 120, (force) = true]; reserved 4; // Deprecated: was string unique_id string icon = 5 [(field_ifdef) = "USE_ENTITY_ICON"]; bool disabled_by_default = 6; @@ -2198,9 +2198,9 @@ message ListEntitiesDateResponse { option (source) = SOURCE_SERVER; option (ifdef) = "USE_DATETIME_DATE"; - string object_id = 1; + string object_id = 1 [(max_data_length) = 120, (force) = true]; fixed32 key = 2 [(force) = true]; - string name = 3; + string name = 3 [(max_data_length) = 120, (force) = true]; reserved 4; // Deprecated: was string unique_id string icon = 5 [(field_ifdef) = "USE_ENTITY_ICON"]; @@ -2245,9 +2245,9 @@ message ListEntitiesTimeResponse { option (source) = SOURCE_SERVER; option (ifdef) = "USE_DATETIME_TIME"; - string object_id = 1; + string object_id = 1 [(max_data_length) = 120, (force) = true]; fixed32 key = 2 [(force) = true]; - string name = 3; + string name = 3 [(max_data_length) = 120, (force) = true]; reserved 4; // Deprecated: was string unique_id string icon = 5 [(field_ifdef) = "USE_ENTITY_ICON"]; @@ -2292,9 +2292,9 @@ message ListEntitiesEventResponse { option (source) = SOURCE_SERVER; option (ifdef) = "USE_EVENT"; - string object_id = 1; + string object_id = 1 [(max_data_length) = 120, (force) = true]; fixed32 key = 2 [(force) = true]; - string name = 3; + string name = 3 [(max_data_length) = 120, (force) = true]; reserved 4; // Deprecated: was string unique_id string icon = 5 [(field_ifdef) = "USE_ENTITY_ICON"]; @@ -2323,9 +2323,9 @@ message ListEntitiesValveResponse { option (source) = SOURCE_SERVER; option (ifdef) = "USE_VALVE"; - string object_id = 1; + string object_id = 1 [(max_data_length) = 120, (force) = true]; fixed32 key = 2 [(force) = true]; - string name = 3; + string name = 3 [(max_data_length) = 120, (force) = true]; reserved 4; // Deprecated: was string unique_id string icon = 5 [(field_ifdef) = "USE_ENTITY_ICON"]; @@ -2378,9 +2378,9 @@ message ListEntitiesDateTimeResponse { option (source) = SOURCE_SERVER; option (ifdef) = "USE_DATETIME_DATETIME"; - string object_id = 1; + string object_id = 1 [(max_data_length) = 120, (force) = true]; fixed32 key = 2 [(force) = true]; - string name = 3; + string name = 3 [(max_data_length) = 120, (force) = true]; reserved 4; // Deprecated: was string unique_id string icon = 5 [(field_ifdef) = "USE_ENTITY_ICON"]; @@ -2421,9 +2421,9 @@ message ListEntitiesUpdateResponse { option (source) = SOURCE_SERVER; option (ifdef) = "USE_UPDATE"; - string object_id = 1; + string object_id = 1 [(max_data_length) = 120, (force) = true]; fixed32 key = 2 [(force) = true]; - string name = 3; + string name = 3 [(max_data_length) = 120, (force) = true]; reserved 4; // Deprecated: was string unique_id string icon = 5 [(field_ifdef) = "USE_ENTITY_ICON"]; @@ -2504,9 +2504,9 @@ message ListEntitiesInfraredResponse { option (source) = SOURCE_SERVER; option (ifdef) = "USE_INFRARED"; - string object_id = 1; + string object_id = 1 [(max_data_length) = 120, (force) = true]; fixed32 key = 2 [(force) = true]; - string name = 3; + string name = 3 [(max_data_length) = 120, (force) = true]; string icon = 4 [(field_ifdef) = "USE_ENTITY_ICON"]; bool disabled_by_default = 5; EntityCategory entity_category = 6; diff --git a/esphome/components/api/api_options.proto b/esphome/components/api/api_options.proto index 0aa9e814cf..0f71268d70 100644 --- a/esphome/components/api/api_options.proto +++ b/esphome/components/api/api_options.proto @@ -102,4 +102,10 @@ extend google.protobuf.FieldOptions { // and direct byte writes instead of varint branching, since the encoded varint // is guaranteed to be 1 byte. optional uint32 max_value = 50017; + + // max_data_length: Maximum length of a string or bytes field. + // When max_data_length < 128, the code generator emits constant-size + // length varint calculations and direct byte writes, since the length + // varint is guaranteed to be 1 byte. + optional uint32 max_data_length = 50018; } diff --git a/esphome/components/api/api_pb2.cpp b/esphome/components/api/api_pb2.cpp index f25d269e8f..00d700116f 100644 --- a/esphome/components/api/api_pb2.cpp +++ b/esphome/components/api/api_pb2.cpp @@ -207,9 +207,13 @@ uint32_t DeviceInfoResponse::calculate_size() const { } #ifdef USE_BINARY_SENSOR void ListEntitiesBinarySensorResponse::encode(ProtoWriteBuffer &buffer) const { - buffer.encode_string(1, this->object_id); + buffer.write_raw_byte(10); + buffer.write_raw_byte(static_cast(this->object_id.size())); + buffer.encode_raw(this->object_id.c_str(), this->object_id.size()); buffer.write_tag_and_fixed32(21, this->key); - buffer.encode_string(3, this->name); + buffer.write_raw_byte(26); + buffer.write_raw_byte(static_cast(this->name.size())); + buffer.encode_raw(this->name.c_str(), this->name.size()); buffer.encode_string(5, this->device_class); buffer.encode_bool(6, this->is_status_binary_sensor); buffer.encode_bool(7, this->disabled_by_default); @@ -223,9 +227,9 @@ void ListEntitiesBinarySensorResponse::encode(ProtoWriteBuffer &buffer) const { } uint32_t ListEntitiesBinarySensorResponse::calculate_size() const { uint32_t size = 0; - size += ProtoSize::calc_length(1, this->object_id.size()); + size += 2 + this->object_id.size(); size += 5; - size += ProtoSize::calc_length(1, this->name.size()); + size += 2 + this->name.size(); size += ProtoSize::calc_length(1, this->device_class.size()); size += ProtoSize::calc_bool(1, this->is_status_binary_sensor); size += ProtoSize::calc_bool(1, this->disabled_by_default); @@ -259,9 +263,13 @@ uint32_t BinarySensorStateResponse::calculate_size() const { #endif #ifdef USE_COVER void ListEntitiesCoverResponse::encode(ProtoWriteBuffer &buffer) const { - buffer.encode_string(1, this->object_id); + buffer.write_raw_byte(10); + buffer.write_raw_byte(static_cast(this->object_id.size())); + buffer.encode_raw(this->object_id.c_str(), this->object_id.size()); buffer.write_tag_and_fixed32(21, this->key); - buffer.encode_string(3, this->name); + buffer.write_raw_byte(26); + buffer.write_raw_byte(static_cast(this->name.size())); + buffer.encode_raw(this->name.c_str(), this->name.size()); buffer.encode_bool(5, this->assumed_state); buffer.encode_bool(6, this->supports_position); buffer.encode_bool(7, this->supports_tilt); @@ -278,9 +286,9 @@ void ListEntitiesCoverResponse::encode(ProtoWriteBuffer &buffer) const { } uint32_t ListEntitiesCoverResponse::calculate_size() const { uint32_t size = 0; - size += ProtoSize::calc_length(1, this->object_id.size()); + size += 2 + this->object_id.size(); size += 5; - size += ProtoSize::calc_length(1, this->name.size()); + size += 2 + this->name.size(); size += ProtoSize::calc_bool(1, this->assumed_state); size += ProtoSize::calc_bool(1, this->supports_position); size += ProtoSize::calc_bool(1, this->supports_tilt); @@ -356,9 +364,13 @@ 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.write_raw_byte(10); + buffer.write_raw_byte(static_cast(this->object_id.size())); + buffer.encode_raw(this->object_id.c_str(), this->object_id.size()); buffer.write_tag_and_fixed32(21, this->key); - buffer.encode_string(3, this->name); + buffer.write_raw_byte(26); + buffer.write_raw_byte(static_cast(this->name.size())); + buffer.encode_raw(this->name.c_str(), this->name.size()); buffer.encode_bool(5, this->supports_oscillation); buffer.encode_bool(6, this->supports_speed); buffer.encode_bool(7, this->supports_direction); @@ -377,9 +389,9 @@ void ListEntitiesFanResponse::encode(ProtoWriteBuffer &buffer) const { } uint32_t ListEntitiesFanResponse::calculate_size() const { uint32_t size = 0; - size += ProtoSize::calc_length(1, this->object_id.size()); + size += 2 + this->object_id.size(); size += 5; - size += ProtoSize::calc_length(1, this->name.size()); + size += 2 + this->name.size(); size += ProtoSize::calc_bool(1, this->supports_oscillation); size += ProtoSize::calc_bool(1, this->supports_speed); size += ProtoSize::calc_bool(1, this->supports_direction); @@ -486,9 +498,13 @@ 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.write_raw_byte(10); + buffer.write_raw_byte(static_cast(this->object_id.size())); + buffer.encode_raw(this->object_id.c_str(), this->object_id.size()); buffer.write_tag_and_fixed32(21, this->key); - buffer.encode_string(3, this->name); + buffer.write_raw_byte(26); + buffer.write_raw_byte(static_cast(this->name.size())); + buffer.encode_raw(this->name.c_str(), this->name.size()); for (const auto &it : *this->supported_color_modes) { buffer.encode_uint32(12, static_cast(it), true); } @@ -508,9 +524,9 @@ void ListEntitiesLightResponse::encode(ProtoWriteBuffer &buffer) const { } uint32_t ListEntitiesLightResponse::calculate_size() const { uint32_t size = 0; - size += ProtoSize::calc_length(1, this->object_id.size()); + size += 2 + this->object_id.size(); size += 5; - size += ProtoSize::calc_length(1, this->name.size()); + size += 2 + this->name.size(); if (!this->supported_color_modes->empty()) { for (const auto &it : *this->supported_color_modes) { size += ProtoSize::calc_uint32_force(1, static_cast(it)); @@ -682,9 +698,13 @@ 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.write_raw_byte(10); + buffer.write_raw_byte(static_cast(this->object_id.size())); + buffer.encode_raw(this->object_id.c_str(), this->object_id.size()); buffer.write_tag_and_fixed32(21, this->key); - buffer.encode_string(3, this->name); + buffer.write_raw_byte(26); + buffer.write_raw_byte(static_cast(this->name.size())); + buffer.encode_raw(this->name.c_str(), this->name.size()); #ifdef USE_ENTITY_ICON buffer.encode_string(5, this->icon); #endif @@ -701,9 +721,9 @@ void ListEntitiesSensorResponse::encode(ProtoWriteBuffer &buffer) const { } uint32_t ListEntitiesSensorResponse::calculate_size() const { uint32_t size = 0; - size += ProtoSize::calc_length(1, this->object_id.size()); + size += 2 + this->object_id.size(); size += 5; - size += ProtoSize::calc_length(1, this->name.size()); + size += 2 + this->name.size(); #ifdef USE_ENTITY_ICON size += ProtoSize::calc_length(1, this->icon.size()); #endif @@ -740,9 +760,13 @@ uint32_t SensorStateResponse::calculate_size() const { #endif #ifdef USE_SWITCH void ListEntitiesSwitchResponse::encode(ProtoWriteBuffer &buffer) const { - buffer.encode_string(1, this->object_id); + buffer.write_raw_byte(10); + buffer.write_raw_byte(static_cast(this->object_id.size())); + buffer.encode_raw(this->object_id.c_str(), this->object_id.size()); buffer.write_tag_and_fixed32(21, this->key); - buffer.encode_string(3, this->name); + buffer.write_raw_byte(26); + buffer.write_raw_byte(static_cast(this->name.size())); + buffer.encode_raw(this->name.c_str(), this->name.size()); #ifdef USE_ENTITY_ICON buffer.encode_string(5, this->icon); #endif @@ -756,9 +780,9 @@ void ListEntitiesSwitchResponse::encode(ProtoWriteBuffer &buffer) const { } uint32_t ListEntitiesSwitchResponse::calculate_size() const { uint32_t size = 0; - size += ProtoSize::calc_length(1, this->object_id.size()); + size += 2 + this->object_id.size(); size += 5; - size += ProtoSize::calc_length(1, this->name.size()); + size += 2 + this->name.size(); #ifdef USE_ENTITY_ICON size += ProtoSize::calc_length(1, this->icon.size()); #endif @@ -815,9 +839,13 @@ 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.write_raw_byte(10); + buffer.write_raw_byte(static_cast(this->object_id.size())); + buffer.encode_raw(this->object_id.c_str(), this->object_id.size()); buffer.write_tag_and_fixed32(21, this->key); - buffer.encode_string(3, this->name); + buffer.write_raw_byte(26); + buffer.write_raw_byte(static_cast(this->name.size())); + buffer.encode_raw(this->name.c_str(), this->name.size()); #ifdef USE_ENTITY_ICON buffer.encode_string(5, this->icon); #endif @@ -830,9 +858,9 @@ void ListEntitiesTextSensorResponse::encode(ProtoWriteBuffer &buffer) const { } uint32_t ListEntitiesTextSensorResponse::calculate_size() const { uint32_t size = 0; - size += ProtoSize::calc_length(1, this->object_id.size()); + size += 2 + this->object_id.size(); size += 5; - size += ProtoSize::calc_length(1, this->name.size()); + size += 2 + this->name.size(); #ifdef USE_ENTITY_ICON size += ProtoSize::calc_length(1, this->icon.size()); #endif @@ -1268,9 +1296,13 @@ uint32_t ExecuteServiceResponse::calculate_size() const { #endif #ifdef USE_CAMERA void ListEntitiesCameraResponse::encode(ProtoWriteBuffer &buffer) const { - buffer.encode_string(1, this->object_id); + buffer.write_raw_byte(10); + buffer.write_raw_byte(static_cast(this->object_id.size())); + buffer.encode_raw(this->object_id.c_str(), this->object_id.size()); buffer.write_tag_and_fixed32(21, this->key); - buffer.encode_string(3, this->name); + buffer.write_raw_byte(26); + buffer.write_raw_byte(static_cast(this->name.size())); + buffer.encode_raw(this->name.c_str(), this->name.size()); buffer.encode_bool(5, this->disabled_by_default); #ifdef USE_ENTITY_ICON buffer.encode_string(6, this->icon); @@ -1282,9 +1314,9 @@ void ListEntitiesCameraResponse::encode(ProtoWriteBuffer &buffer) const { } uint32_t ListEntitiesCameraResponse::calculate_size() const { uint32_t size = 0; - size += ProtoSize::calc_length(1, this->object_id.size()); + size += 2 + this->object_id.size(); size += 5; - size += ProtoSize::calc_length(1, this->name.size()); + size += 2 + this->name.size(); size += ProtoSize::calc_bool(1, this->disabled_by_default); #ifdef USE_ENTITY_ICON size += ProtoSize::calc_length(1, this->icon.size()); @@ -1329,9 +1361,13 @@ bool CameraImageRequest::decode_varint(uint32_t field_id, proto_varint_value_t v #endif #ifdef USE_CLIMATE void ListEntitiesClimateResponse::encode(ProtoWriteBuffer &buffer) const { - buffer.encode_string(1, this->object_id); + buffer.write_raw_byte(10); + buffer.write_raw_byte(static_cast(this->object_id.size())); + buffer.encode_raw(this->object_id.c_str(), this->object_id.size()); buffer.write_tag_and_fixed32(21, this->key); - buffer.encode_string(3, this->name); + buffer.write_raw_byte(26); + buffer.write_raw_byte(static_cast(this->name.size())); + buffer.encode_raw(this->name.c_str(), this->name.size()); buffer.encode_bool(5, this->supports_current_temperature); buffer.encode_bool(6, this->supports_two_point_target_temperature); for (const auto &it : *this->supported_modes) { @@ -1373,9 +1409,9 @@ void ListEntitiesClimateResponse::encode(ProtoWriteBuffer &buffer) const { } uint32_t ListEntitiesClimateResponse::calculate_size() const { uint32_t size = 0; - size += ProtoSize::calc_length(1, this->object_id.size()); + size += 2 + this->object_id.size(); size += 5; - size += ProtoSize::calc_length(1, this->name.size()); + size += 2 + this->name.size(); size += ProtoSize::calc_bool(1, this->supports_current_temperature); size += ProtoSize::calc_bool(1, this->supports_two_point_target_temperature); if (!this->supported_modes->empty()) { @@ -1562,9 +1598,13 @@ bool ClimateCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) { #endif #ifdef USE_WATER_HEATER void ListEntitiesWaterHeaterResponse::encode(ProtoWriteBuffer &buffer) const { - buffer.encode_string(1, this->object_id); + buffer.write_raw_byte(10); + buffer.write_raw_byte(static_cast(this->object_id.size())); + buffer.encode_raw(this->object_id.c_str(), this->object_id.size()); buffer.write_tag_and_fixed32(21, this->key); - buffer.encode_string(3, this->name); + buffer.write_raw_byte(26); + buffer.write_raw_byte(static_cast(this->name.size())); + buffer.encode_raw(this->name.c_str(), this->name.size()); #ifdef USE_ENTITY_ICON buffer.encode_string(4, this->icon); #endif @@ -1583,9 +1623,9 @@ void ListEntitiesWaterHeaterResponse::encode(ProtoWriteBuffer &buffer) const { } uint32_t ListEntitiesWaterHeaterResponse::calculate_size() const { uint32_t size = 0; - size += ProtoSize::calc_length(1, this->object_id.size()); + size += 2 + this->object_id.size(); size += 5; - size += ProtoSize::calc_length(1, this->name.size()); + size += 2 + this->name.size(); #ifdef USE_ENTITY_ICON size += ProtoSize::calc_length(1, this->icon.size()); #endif @@ -1674,9 +1714,13 @@ bool WaterHeaterCommandRequest::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.write_raw_byte(10); + buffer.write_raw_byte(static_cast(this->object_id.size())); + buffer.encode_raw(this->object_id.c_str(), this->object_id.size()); buffer.write_tag_and_fixed32(21, this->key); - buffer.encode_string(3, this->name); + buffer.write_raw_byte(26); + buffer.write_raw_byte(static_cast(this->name.size())); + buffer.encode_raw(this->name.c_str(), this->name.size()); #ifdef USE_ENTITY_ICON buffer.encode_string(5, this->icon); #endif @@ -1694,9 +1738,9 @@ void ListEntitiesNumberResponse::encode(ProtoWriteBuffer &buffer) const { } uint32_t ListEntitiesNumberResponse::calculate_size() const { uint32_t size = 0; - size += ProtoSize::calc_length(1, this->object_id.size()); + size += 2 + this->object_id.size(); size += 5; - size += ProtoSize::calc_length(1, this->name.size()); + size += 2 + this->name.size(); #ifdef USE_ENTITY_ICON size += ProtoSize::calc_length(1, this->icon.size()); #endif @@ -1759,9 +1803,13 @@ 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.write_raw_byte(10); + buffer.write_raw_byte(static_cast(this->object_id.size())); + buffer.encode_raw(this->object_id.c_str(), this->object_id.size()); buffer.write_tag_and_fixed32(21, this->key); - buffer.encode_string(3, this->name); + buffer.write_raw_byte(26); + buffer.write_raw_byte(static_cast(this->name.size())); + buffer.encode_raw(this->name.c_str(), this->name.size()); #ifdef USE_ENTITY_ICON buffer.encode_string(5, this->icon); #endif @@ -1776,9 +1824,9 @@ void ListEntitiesSelectResponse::encode(ProtoWriteBuffer &buffer) const { } uint32_t ListEntitiesSelectResponse::calculate_size() const { uint32_t size = 0; - size += ProtoSize::calc_length(1, this->object_id.size()); + size += 2 + this->object_id.size(); size += 5; - size += ProtoSize::calc_length(1, this->name.size()); + size += 2 + this->name.size(); #ifdef USE_ENTITY_ICON size += ProtoSize::calc_length(1, this->icon.size()); #endif @@ -1848,9 +1896,13 @@ 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.write_raw_byte(10); + buffer.write_raw_byte(static_cast(this->object_id.size())); + buffer.encode_raw(this->object_id.c_str(), this->object_id.size()); buffer.write_tag_and_fixed32(21, this->key); - buffer.encode_string(3, this->name); + buffer.write_raw_byte(26); + buffer.write_raw_byte(static_cast(this->name.size())); + buffer.encode_raw(this->name.c_str(), this->name.size()); #ifdef USE_ENTITY_ICON buffer.encode_string(5, this->icon); #endif @@ -1867,9 +1919,9 @@ void ListEntitiesSirenResponse::encode(ProtoWriteBuffer &buffer) const { } uint32_t ListEntitiesSirenResponse::calculate_size() const { uint32_t size = 0; - size += ProtoSize::calc_length(1, this->object_id.size()); + size += 2 + this->object_id.size(); size += 5; - size += ProtoSize::calc_length(1, this->name.size()); + size += 2 + this->name.size(); #ifdef USE_ENTITY_ICON size += ProtoSize::calc_length(1, this->icon.size()); #endif @@ -1960,9 +2012,13 @@ 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.write_raw_byte(10); + buffer.write_raw_byte(static_cast(this->object_id.size())); + buffer.encode_raw(this->object_id.c_str(), this->object_id.size()); buffer.write_tag_and_fixed32(21, this->key); - buffer.encode_string(3, this->name); + buffer.write_raw_byte(26); + buffer.write_raw_byte(static_cast(this->name.size())); + buffer.encode_raw(this->name.c_str(), this->name.size()); #ifdef USE_ENTITY_ICON buffer.encode_string(5, this->icon); #endif @@ -1978,9 +2034,9 @@ void ListEntitiesLockResponse::encode(ProtoWriteBuffer &buffer) const { } uint32_t ListEntitiesLockResponse::calculate_size() const { uint32_t size = 0; - size += ProtoSize::calc_length(1, this->object_id.size()); + size += 2 + this->object_id.size(); size += 5; - size += ProtoSize::calc_length(1, this->name.size()); + size += 2 + this->name.size(); #ifdef USE_ENTITY_ICON size += ProtoSize::calc_length(1, this->icon.size()); #endif @@ -2053,9 +2109,13 @@ 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.write_raw_byte(10); + buffer.write_raw_byte(static_cast(this->object_id.size())); + buffer.encode_raw(this->object_id.c_str(), this->object_id.size()); buffer.write_tag_and_fixed32(21, this->key); - buffer.encode_string(3, this->name); + buffer.write_raw_byte(26); + buffer.write_raw_byte(static_cast(this->name.size())); + buffer.encode_raw(this->name.c_str(), this->name.size()); #ifdef USE_ENTITY_ICON buffer.encode_string(5, this->icon); #endif @@ -2068,9 +2128,9 @@ void ListEntitiesButtonResponse::encode(ProtoWriteBuffer &buffer) const { } uint32_t ListEntitiesButtonResponse::calculate_size() const { uint32_t size = 0; - size += ProtoSize::calc_length(1, this->object_id.size()); + size += 2 + this->object_id.size(); size += 5; - size += ProtoSize::calc_length(1, this->name.size()); + size += 2 + this->name.size(); #ifdef USE_ENTITY_ICON size += ProtoSize::calc_length(1, this->icon.size()); #endif @@ -2123,9 +2183,13 @@ uint32_t MediaPlayerSupportedFormat::calculate_size() const { return size; } void ListEntitiesMediaPlayerResponse::encode(ProtoWriteBuffer &buffer) const { - buffer.encode_string(1, this->object_id); + buffer.write_raw_byte(10); + buffer.write_raw_byte(static_cast(this->object_id.size())); + buffer.encode_raw(this->object_id.c_str(), this->object_id.size()); buffer.write_tag_and_fixed32(21, this->key); - buffer.encode_string(3, this->name); + buffer.write_raw_byte(26); + buffer.write_raw_byte(static_cast(this->name.size())); + buffer.encode_raw(this->name.c_str(), this->name.size()); #ifdef USE_ENTITY_ICON buffer.encode_string(5, this->icon); #endif @@ -2142,9 +2206,9 @@ void ListEntitiesMediaPlayerResponse::encode(ProtoWriteBuffer &buffer) const { } uint32_t ListEntitiesMediaPlayerResponse::calculate_size() const { uint32_t size = 0; - size += ProtoSize::calc_length(1, this->object_id.size()); + size += 2 + this->object_id.size(); size += 5; - size += ProtoSize::calc_length(1, this->name.size()); + size += 2 + this->name.size(); #ifdef USE_ENTITY_ICON size += ProtoSize::calc_length(1, this->icon.size()); #endif @@ -2945,9 +3009,13 @@ 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.write_raw_byte(10); + buffer.write_raw_byte(static_cast(this->object_id.size())); + buffer.encode_raw(this->object_id.c_str(), this->object_id.size()); buffer.write_tag_and_fixed32(21, this->key); - buffer.encode_string(3, this->name); + buffer.write_raw_byte(26); + buffer.write_raw_byte(static_cast(this->name.size())); + buffer.encode_raw(this->name.c_str(), this->name.size()); #ifdef USE_ENTITY_ICON buffer.encode_string(5, this->icon); #endif @@ -2962,9 +3030,9 @@ void ListEntitiesAlarmControlPanelResponse::encode(ProtoWriteBuffer &buffer) con } uint32_t ListEntitiesAlarmControlPanelResponse::calculate_size() const { uint32_t size = 0; - size += ProtoSize::calc_length(1, this->object_id.size()); + size += 2 + this->object_id.size(); size += 5; - size += ProtoSize::calc_length(1, this->name.size()); + size += 2 + this->name.size(); #ifdef USE_ENTITY_ICON size += ProtoSize::calc_length(1, this->icon.size()); #endif @@ -3033,9 +3101,13 @@ 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.write_raw_byte(10); + buffer.write_raw_byte(static_cast(this->object_id.size())); + buffer.encode_raw(this->object_id.c_str(), this->object_id.size()); buffer.write_tag_and_fixed32(21, this->key); - buffer.encode_string(3, this->name); + buffer.write_raw_byte(26); + buffer.write_raw_byte(static_cast(this->name.size())); + buffer.encode_raw(this->name.c_str(), this->name.size()); #ifdef USE_ENTITY_ICON buffer.encode_string(5, this->icon); #endif @@ -3051,9 +3123,9 @@ void ListEntitiesTextResponse::encode(ProtoWriteBuffer &buffer) const { } uint32_t ListEntitiesTextResponse::calculate_size() const { uint32_t size = 0; - size += ProtoSize::calc_length(1, this->object_id.size()); + size += 2 + this->object_id.size(); size += 5; - size += ProtoSize::calc_length(1, this->name.size()); + size += 2 + this->name.size(); #ifdef USE_ENTITY_ICON size += ProtoSize::calc_length(1, this->icon.size()); #endif @@ -3122,9 +3194,13 @@ 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.write_raw_byte(10); + buffer.write_raw_byte(static_cast(this->object_id.size())); + buffer.encode_raw(this->object_id.c_str(), this->object_id.size()); buffer.write_tag_and_fixed32(21, this->key); - buffer.encode_string(3, this->name); + buffer.write_raw_byte(26); + buffer.write_raw_byte(static_cast(this->name.size())); + buffer.encode_raw(this->name.c_str(), this->name.size()); #ifdef USE_ENTITY_ICON buffer.encode_string(5, this->icon); #endif @@ -3136,9 +3212,9 @@ void ListEntitiesDateResponse::encode(ProtoWriteBuffer &buffer) const { } uint32_t ListEntitiesDateResponse::calculate_size() const { uint32_t size = 0; - size += ProtoSize::calc_length(1, this->object_id.size()); + size += 2 + this->object_id.size(); size += 5; - size += ProtoSize::calc_length(1, this->name.size()); + size += 2 + this->name.size(); #ifdef USE_ENTITY_ICON size += ProtoSize::calc_length(1, this->icon.size()); #endif @@ -3205,9 +3281,13 @@ 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.write_raw_byte(10); + buffer.write_raw_byte(static_cast(this->object_id.size())); + buffer.encode_raw(this->object_id.c_str(), this->object_id.size()); buffer.write_tag_and_fixed32(21, this->key); - buffer.encode_string(3, this->name); + buffer.write_raw_byte(26); + buffer.write_raw_byte(static_cast(this->name.size())); + buffer.encode_raw(this->name.c_str(), this->name.size()); #ifdef USE_ENTITY_ICON buffer.encode_string(5, this->icon); #endif @@ -3219,9 +3299,9 @@ void ListEntitiesTimeResponse::encode(ProtoWriteBuffer &buffer) const { } uint32_t ListEntitiesTimeResponse::calculate_size() const { uint32_t size = 0; - size += ProtoSize::calc_length(1, this->object_id.size()); + size += 2 + this->object_id.size(); size += 5; - size += ProtoSize::calc_length(1, this->name.size()); + size += 2 + this->name.size(); #ifdef USE_ENTITY_ICON size += ProtoSize::calc_length(1, this->icon.size()); #endif @@ -3288,9 +3368,13 @@ 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.write_raw_byte(10); + buffer.write_raw_byte(static_cast(this->object_id.size())); + buffer.encode_raw(this->object_id.c_str(), this->object_id.size()); buffer.write_tag_and_fixed32(21, this->key); - buffer.encode_string(3, this->name); + buffer.write_raw_byte(26); + buffer.write_raw_byte(static_cast(this->name.size())); + buffer.encode_raw(this->name.c_str(), this->name.size()); #ifdef USE_ENTITY_ICON buffer.encode_string(5, this->icon); #endif @@ -3306,9 +3390,9 @@ void ListEntitiesEventResponse::encode(ProtoWriteBuffer &buffer) const { } uint32_t ListEntitiesEventResponse::calculate_size() const { uint32_t size = 0; - size += ProtoSize::calc_length(1, this->object_id.size()); + size += 2 + this->object_id.size(); size += 5; - size += ProtoSize::calc_length(1, this->name.size()); + size += 2 + this->name.size(); #ifdef USE_ENTITY_ICON size += ProtoSize::calc_length(1, this->icon.size()); #endif @@ -3344,9 +3428,13 @@ uint32_t EventResponse::calculate_size() const { #endif #ifdef USE_VALVE void ListEntitiesValveResponse::encode(ProtoWriteBuffer &buffer) const { - buffer.encode_string(1, this->object_id); + buffer.write_raw_byte(10); + buffer.write_raw_byte(static_cast(this->object_id.size())); + buffer.encode_raw(this->object_id.c_str(), this->object_id.size()); buffer.write_tag_and_fixed32(21, this->key); - buffer.encode_string(3, this->name); + buffer.write_raw_byte(26); + buffer.write_raw_byte(static_cast(this->name.size())); + buffer.encode_raw(this->name.c_str(), this->name.size()); #ifdef USE_ENTITY_ICON buffer.encode_string(5, this->icon); #endif @@ -3362,9 +3450,9 @@ void ListEntitiesValveResponse::encode(ProtoWriteBuffer &buffer) const { } uint32_t ListEntitiesValveResponse::calculate_size() const { uint32_t size = 0; - size += ProtoSize::calc_length(1, this->object_id.size()); + size += 2 + this->object_id.size(); size += 5; - size += ProtoSize::calc_length(1, this->name.size()); + size += 2 + this->name.size(); #ifdef USE_ENTITY_ICON size += ProtoSize::calc_length(1, this->icon.size()); #endif @@ -3431,9 +3519,13 @@ 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.write_raw_byte(10); + buffer.write_raw_byte(static_cast(this->object_id.size())); + buffer.encode_raw(this->object_id.c_str(), this->object_id.size()); buffer.write_tag_and_fixed32(21, this->key); - buffer.encode_string(3, this->name); + buffer.write_raw_byte(26); + buffer.write_raw_byte(static_cast(this->name.size())); + buffer.encode_raw(this->name.c_str(), this->name.size()); #ifdef USE_ENTITY_ICON buffer.encode_string(5, this->icon); #endif @@ -3445,9 +3537,9 @@ void ListEntitiesDateTimeResponse::encode(ProtoWriteBuffer &buffer) const { } uint32_t ListEntitiesDateTimeResponse::calculate_size() const { uint32_t size = 0; - size += ProtoSize::calc_length(1, this->object_id.size()); + size += 2 + this->object_id.size(); size += 5; - size += ProtoSize::calc_length(1, this->name.size()); + size += 2 + this->name.size(); #ifdef USE_ENTITY_ICON size += ProtoSize::calc_length(1, this->icon.size()); #endif @@ -3504,9 +3596,13 @@ 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.write_raw_byte(10); + buffer.write_raw_byte(static_cast(this->object_id.size())); + buffer.encode_raw(this->object_id.c_str(), this->object_id.size()); buffer.write_tag_and_fixed32(21, this->key); - buffer.encode_string(3, this->name); + buffer.write_raw_byte(26); + buffer.write_raw_byte(static_cast(this->name.size())); + buffer.encode_raw(this->name.c_str(), this->name.size()); #ifdef USE_ENTITY_ICON buffer.encode_string(5, this->icon); #endif @@ -3519,9 +3615,9 @@ void ListEntitiesUpdateResponse::encode(ProtoWriteBuffer &buffer) const { } uint32_t ListEntitiesUpdateResponse::calculate_size() const { uint32_t size = 0; - size += ProtoSize::calc_length(1, this->object_id.size()); + size += 2 + this->object_id.size(); size += 5; - size += ProtoSize::calc_length(1, this->name.size()); + size += 2 + this->name.size(); #ifdef USE_ENTITY_ICON size += ProtoSize::calc_length(1, this->icon.size()); #endif @@ -3645,9 +3741,13 @@ uint32_t ZWaveProxyRequest::calculate_size() const { #endif #ifdef USE_INFRARED void ListEntitiesInfraredResponse::encode(ProtoWriteBuffer &buffer) const { - buffer.encode_string(1, this->object_id); + buffer.write_raw_byte(10); + buffer.write_raw_byte(static_cast(this->object_id.size())); + buffer.encode_raw(this->object_id.c_str(), this->object_id.size()); buffer.write_tag_and_fixed32(21, this->key); - buffer.encode_string(3, this->name); + buffer.write_raw_byte(26); + buffer.write_raw_byte(static_cast(this->name.size())); + buffer.encode_raw(this->name.c_str(), this->name.size()); #ifdef USE_ENTITY_ICON buffer.encode_string(4, this->icon); #endif @@ -3661,9 +3761,9 @@ void ListEntitiesInfraredResponse::encode(ProtoWriteBuffer &buffer) const { } uint32_t ListEntitiesInfraredResponse::calculate_size() const { uint32_t size = 0; - size += ProtoSize::calc_length(1, this->object_id.size()); + size += 2 + this->object_id.size(); size += 5; - size += ProtoSize::calc_length(1, this->name.size()); + size += 2 + this->name.size(); #ifdef USE_ENTITY_ICON size += ProtoSize::calc_length(1, this->icon.size()); #endif diff --git a/script/api_protobuf/api_protobuf.py b/script/api_protobuf/api_protobuf.py index c17f16412c..a0335ff404 100755 --- a/script/api_protobuf/api_protobuf.py +++ b/script/api_protobuf/api_protobuf.py @@ -161,6 +161,11 @@ class TypeInfo(ABC): """Get the max_value option for this field, or None if not set.""" return get_field_opt(self._field, pb.max_value, None) + @property + def max_data_length(self) -> int | None: + """Get the max_data_length option for this field, or None if not set.""" + return get_field_opt(self._field, pb.max_data_length, None) + @property def wire_type(self) -> WireType: """Get the wire type for the field.""" @@ -1064,7 +1069,9 @@ class PointerToStringBufferType(PointerToBufferTypeBase): @property def encode_content(self) -> str: if result := self._encode_bytes_with_precomputed_tag( - f"this->{self.field_name}.c_str()", f"this->{self.field_name}.size()" + f"this->{self.field_name}.c_str()", + f"this->{self.field_name}.size()", + max_len=self.max_data_length, ): return result if self.force: @@ -1089,7 +1096,13 @@ class PointerToStringBufferType(PointerToBufferTypeBase): return f'dump_field(out, ESPHOME_PSTR("{self.name}"), this->{self.field_name});' def get_size_calculation(self, name: str, force: bool = False) -> str: - return f"size += ProtoSize::calc_length({self.calculate_field_id_size()}, this->{self.field_name}.size());" + size_field = f"this->{self.field_name}.size()" + max_len = self.max_data_length + if max_len is not None and max_len < 128: + return self._get_single_byte_varint_size( + size_field, force, extra_expr=size_field + ) + return self._get_simple_size_calculation(size_field, force, "length") def get_estimated_size(self) -> int: return self.calculate_field_id_size() + 8 # field ID + 8 bytes typical string From 5dbe2a774124e47468241eb48204347c93ae0420 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 3 Apr 2026 13:36:32 -1000 Subject: [PATCH 17/31] [api] Add comment tying max_data_length = 120 to NAME_MAX_LENGTH --- esphome/components/api/api.proto | 3 +++ 1 file changed, 3 insertions(+) diff --git a/esphome/components/api/api.proto b/esphome/components/api/api.proto index 9b62f8e758..afb66ebb13 100644 --- a/esphome/components/api/api.proto +++ b/esphome/components/api/api.proto @@ -308,6 +308,9 @@ enum EntityCategory { ENTITY_CATEGORY_DIAGNOSTIC = 2; } +// Entity name/object_id max_data_length = 120 matches NAME_MAX_LENGTH +// in esphome/config_validation.py (validated at config time). + // ==================== BINARY SENSOR ==================== message ListEntitiesBinarySensorResponse { option (id) = 12; From 0124087e733d025594614967f418cf28acccafca Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 3 Apr 2026 13:40:27 -1000 Subject: [PATCH 18/31] [api] Add max_data_length = 63 for icon fields --- esphome/components/api/api.proto | 55 +++++++++++++++--------------- esphome/components/api/api_pb2.cpp | 50 +++++++++++++-------------- 2 files changed, 53 insertions(+), 52 deletions(-) diff --git a/esphome/components/api/api.proto b/esphome/components/api/api.proto index afb66ebb13..278e977a73 100644 --- a/esphome/components/api/api.proto +++ b/esphome/components/api/api.proto @@ -308,8 +308,9 @@ enum EntityCategory { ENTITY_CATEGORY_DIAGNOSTIC = 2; } -// Entity name/object_id max_data_length = 120 matches NAME_MAX_LENGTH -// in esphome/config_validation.py (validated at config time). +// Entity name/object_id max_data_length = 120 matches NAME_MAX_LENGTH, +// icon max_data_length = 63 matches ICON_MAX_LENGTH. +// Both defined in esphome/core/config.py and validated at config time. // ==================== BINARY SENSOR ==================== message ListEntitiesBinarySensorResponse { @@ -326,7 +327,7 @@ message ListEntitiesBinarySensorResponse { string device_class = 5; bool is_status_binary_sensor = 6; bool disabled_by_default = 7; - string icon = 8 [(field_ifdef) = "USE_ENTITY_ICON"]; + string icon = 8 [(field_ifdef) = "USE_ENTITY_ICON", (max_data_length) = 63]; EntityCategory entity_category = 9; uint32 device_id = 10 [(field_ifdef) = "USE_DEVICES"]; } @@ -362,7 +363,7 @@ message ListEntitiesCoverResponse { bool supports_tilt = 7; string device_class = 8; bool disabled_by_default = 9; - string icon = 10 [(field_ifdef) = "USE_ENTITY_ICON"]; + string icon = 10 [(field_ifdef) = "USE_ENTITY_ICON", (max_data_length) = 63]; EntityCategory entity_category = 11; bool supports_stop = 12; uint32 device_id = 13 [(field_ifdef) = "USE_DEVICES"]; @@ -446,7 +447,7 @@ message ListEntitiesFanResponse { bool supports_direction = 7; int32 supported_speed_count = 8; bool disabled_by_default = 9; - string icon = 10 [(field_ifdef) = "USE_ENTITY_ICON"]; + string icon = 10 [(field_ifdef) = "USE_ENTITY_ICON", (max_data_length) = 63]; EntityCategory entity_category = 11; repeated string supported_preset_modes = 12 [(container_pointer_no_template) = "std::vector"]; uint32 device_id = 13 [(field_ifdef) = "USE_DEVICES"]; @@ -543,7 +544,7 @@ message ListEntitiesLightResponse { float max_mireds = 10; repeated string effects = 11 [(container_pointer_no_template) = "FixedVector"]; bool disabled_by_default = 13; - string icon = 14 [(field_ifdef) = "USE_ENTITY_ICON"]; + string icon = 14 [(field_ifdef) = "USE_ENTITY_ICON", (max_data_length) = 63]; EntityCategory entity_category = 15; uint32 device_id = 16 [(field_ifdef) = "USE_DEVICES"]; } @@ -634,7 +635,7 @@ message ListEntitiesSensorResponse { string name = 3 [(max_data_length) = 120, (force) = true]; reserved 4; // Deprecated: was string unique_id - string icon = 5 [(field_ifdef) = "USE_ENTITY_ICON"]; + string icon = 5 [(field_ifdef) = "USE_ENTITY_ICON", (max_data_length) = 63]; string unit_of_measurement = 6; int32 accuracy_decimals = 7; bool force_update = 8; @@ -674,7 +675,7 @@ message ListEntitiesSwitchResponse { string name = 3 [(max_data_length) = 120, (force) = true]; reserved 4; // Deprecated: was string unique_id - string icon = 5 [(field_ifdef) = "USE_ENTITY_ICON"]; + string icon = 5 [(field_ifdef) = "USE_ENTITY_ICON", (max_data_length) = 63]; bool assumed_state = 6; bool disabled_by_default = 7; EntityCategory entity_category = 8; @@ -716,7 +717,7 @@ message ListEntitiesTextSensorResponse { string name = 3 [(max_data_length) = 120, (force) = true]; reserved 4; // Deprecated: was string unique_id - string icon = 5 [(field_ifdef) = "USE_ENTITY_ICON"]; + string icon = 5 [(field_ifdef) = "USE_ENTITY_ICON", (max_data_length) = 63]; bool disabled_by_default = 6; EntityCategory entity_category = 7; string device_class = 8; @@ -979,7 +980,7 @@ message ListEntitiesCameraResponse { string name = 3 [(max_data_length) = 120, (force) = true]; reserved 4; // Deprecated: was string unique_id bool disabled_by_default = 5; - string icon = 6 [(field_ifdef) = "USE_ENTITY_ICON"]; + string icon = 6 [(field_ifdef) = "USE_ENTITY_ICON", (max_data_length) = 63]; EntityCategory entity_category = 7; uint32 device_id = 8 [(field_ifdef) = "USE_DEVICES"]; } @@ -1081,7 +1082,7 @@ message ListEntitiesClimateResponse { repeated ClimatePreset supported_presets = 16 [(container_pointer_no_template) = "climate::ClimatePresetMask"]; repeated string supported_custom_presets = 17 [(container_pointer_no_template) = "std::vector"]; bool disabled_by_default = 18; - string icon = 19 [(field_ifdef) = "USE_ENTITY_ICON"]; + string icon = 19 [(field_ifdef) = "USE_ENTITY_ICON", (max_data_length) = 63]; EntityCategory entity_category = 20; float visual_current_temperature_step = 21; bool supports_current_humidity = 22; // Deprecated: use feature_flags @@ -1173,7 +1174,7 @@ message ListEntitiesWaterHeaterResponse { string object_id = 1 [(max_data_length) = 120, (force) = true]; fixed32 key = 2 [(force) = true]; string name = 3 [(max_data_length) = 120, (force) = true]; - string icon = 4 [(field_ifdef) = "USE_ENTITY_ICON"]; + string icon = 4 [(field_ifdef) = "USE_ENTITY_ICON", (max_data_length) = 63]; bool disabled_by_default = 5; EntityCategory entity_category = 6; uint32 device_id = 7 [(field_ifdef) = "USE_DEVICES"]; @@ -1251,7 +1252,7 @@ message ListEntitiesNumberResponse { string name = 3 [(max_data_length) = 120, (force) = true]; reserved 4; // Deprecated: was string unique_id - string icon = 5 [(field_ifdef) = "USE_ENTITY_ICON"]; + string icon = 5 [(field_ifdef) = "USE_ENTITY_ICON", (max_data_length) = 63]; float min_value = 6; float max_value = 7; float step = 8; @@ -1300,7 +1301,7 @@ message ListEntitiesSelectResponse { string name = 3 [(max_data_length) = 120, (force) = true]; reserved 4; // Deprecated: was string unique_id - string icon = 5 [(field_ifdef) = "USE_ENTITY_ICON"]; + string icon = 5 [(field_ifdef) = "USE_ENTITY_ICON", (max_data_length) = 63]; repeated string options = 6 [(container_pointer_no_template) = "FixedVector"]; bool disabled_by_default = 7; EntityCategory entity_category = 8; @@ -1344,7 +1345,7 @@ message ListEntitiesSirenResponse { string name = 3 [(max_data_length) = 120, (force) = true]; reserved 4; // Deprecated: was string unique_id - string icon = 5 [(field_ifdef) = "USE_ENTITY_ICON"]; + string icon = 5 [(field_ifdef) = "USE_ENTITY_ICON", (max_data_length) = 63]; bool disabled_by_default = 6; repeated string tones = 7 [(container_pointer_no_template) = "FixedVector"]; bool supports_duration = 8; @@ -1407,7 +1408,7 @@ message ListEntitiesLockResponse { string name = 3 [(max_data_length) = 120, (force) = true]; reserved 4; // Deprecated: was string unique_id - string icon = 5 [(field_ifdef) = "USE_ENTITY_ICON"]; + string icon = 5 [(field_ifdef) = "USE_ENTITY_ICON", (max_data_length) = 63]; bool disabled_by_default = 6; EntityCategory entity_category = 7; bool assumed_state = 8; @@ -1456,7 +1457,7 @@ message ListEntitiesButtonResponse { string name = 3 [(max_data_length) = 120, (force) = true]; reserved 4; // Deprecated: was string unique_id - string icon = 5 [(field_ifdef) = "USE_ENTITY_ICON"]; + string icon = 5 [(field_ifdef) = "USE_ENTITY_ICON", (max_data_length) = 63]; bool disabled_by_default = 6; EntityCategory entity_category = 7; string device_class = 8; @@ -1523,7 +1524,7 @@ message ListEntitiesMediaPlayerResponse { string name = 3 [(max_data_length) = 120, (force) = true]; reserved 4; // Deprecated: was string unique_id - string icon = 5 [(field_ifdef) = "USE_ENTITY_ICON"]; + string icon = 5 [(field_ifdef) = "USE_ENTITY_ICON", (max_data_length) = 63]; bool disabled_by_default = 6; EntityCategory entity_category = 7; @@ -2110,7 +2111,7 @@ message ListEntitiesAlarmControlPanelResponse { fixed32 key = 2 [(force) = true]; string name = 3 [(max_data_length) = 120, (force) = true]; reserved 4; // Deprecated: was string unique_id - string icon = 5 [(field_ifdef) = "USE_ENTITY_ICON"]; + string icon = 5 [(field_ifdef) = "USE_ENTITY_ICON", (max_data_length) = 63]; bool disabled_by_default = 6; EntityCategory entity_category = 7; uint32 supported_features = 8; @@ -2157,7 +2158,7 @@ message ListEntitiesTextResponse { fixed32 key = 2 [(force) = true]; string name = 3 [(max_data_length) = 120, (force) = true]; reserved 4; // Deprecated: was string unique_id - string icon = 5 [(field_ifdef) = "USE_ENTITY_ICON"]; + string icon = 5 [(field_ifdef) = "USE_ENTITY_ICON", (max_data_length) = 63]; bool disabled_by_default = 6; EntityCategory entity_category = 7; @@ -2206,7 +2207,7 @@ message ListEntitiesDateResponse { string name = 3 [(max_data_length) = 120, (force) = true]; reserved 4; // Deprecated: was string unique_id - string icon = 5 [(field_ifdef) = "USE_ENTITY_ICON"]; + string icon = 5 [(field_ifdef) = "USE_ENTITY_ICON", (max_data_length) = 63]; bool disabled_by_default = 6; EntityCategory entity_category = 7; uint32 device_id = 8 [(field_ifdef) = "USE_DEVICES"]; @@ -2253,7 +2254,7 @@ message ListEntitiesTimeResponse { string name = 3 [(max_data_length) = 120, (force) = true]; reserved 4; // Deprecated: was string unique_id - string icon = 5 [(field_ifdef) = "USE_ENTITY_ICON"]; + string icon = 5 [(field_ifdef) = "USE_ENTITY_ICON", (max_data_length) = 63]; bool disabled_by_default = 6; EntityCategory entity_category = 7; uint32 device_id = 8 [(field_ifdef) = "USE_DEVICES"]; @@ -2300,7 +2301,7 @@ message ListEntitiesEventResponse { string name = 3 [(max_data_length) = 120, (force) = true]; reserved 4; // Deprecated: was string unique_id - string icon = 5 [(field_ifdef) = "USE_ENTITY_ICON"]; + string icon = 5 [(field_ifdef) = "USE_ENTITY_ICON", (max_data_length) = 63]; bool disabled_by_default = 6; EntityCategory entity_category = 7; string device_class = 8; @@ -2331,7 +2332,7 @@ message ListEntitiesValveResponse { string name = 3 [(max_data_length) = 120, (force) = true]; reserved 4; // Deprecated: was string unique_id - string icon = 5 [(field_ifdef) = "USE_ENTITY_ICON"]; + string icon = 5 [(field_ifdef) = "USE_ENTITY_ICON", (max_data_length) = 63]; bool disabled_by_default = 6; EntityCategory entity_category = 7; string device_class = 8; @@ -2386,7 +2387,7 @@ message ListEntitiesDateTimeResponse { string name = 3 [(max_data_length) = 120, (force) = true]; reserved 4; // Deprecated: was string unique_id - string icon = 5 [(field_ifdef) = "USE_ENTITY_ICON"]; + string icon = 5 [(field_ifdef) = "USE_ENTITY_ICON", (max_data_length) = 63]; bool disabled_by_default = 6; EntityCategory entity_category = 7; uint32 device_id = 8 [(field_ifdef) = "USE_DEVICES"]; @@ -2429,7 +2430,7 @@ message ListEntitiesUpdateResponse { string name = 3 [(max_data_length) = 120, (force) = true]; reserved 4; // Deprecated: was string unique_id - string icon = 5 [(field_ifdef) = "USE_ENTITY_ICON"]; + string icon = 5 [(field_ifdef) = "USE_ENTITY_ICON", (max_data_length) = 63]; bool disabled_by_default = 6; EntityCategory entity_category = 7; string device_class = 8; @@ -2510,7 +2511,7 @@ message ListEntitiesInfraredResponse { string object_id = 1 [(max_data_length) = 120, (force) = true]; fixed32 key = 2 [(force) = true]; string name = 3 [(max_data_length) = 120, (force) = true]; - string icon = 4 [(field_ifdef) = "USE_ENTITY_ICON"]; + string icon = 4 [(field_ifdef) = "USE_ENTITY_ICON", (max_data_length) = 63]; bool disabled_by_default = 5; EntityCategory entity_category = 6; uint32 device_id = 7 [(field_ifdef) = "USE_DEVICES"]; diff --git a/esphome/components/api/api_pb2.cpp b/esphome/components/api/api_pb2.cpp index 00d700116f..6ef7ca4d3f 100644 --- a/esphome/components/api/api_pb2.cpp +++ b/esphome/components/api/api_pb2.cpp @@ -234,7 +234,7 @@ uint32_t ListEntitiesBinarySensorResponse::calculate_size() const { size += ProtoSize::calc_bool(1, this->is_status_binary_sensor); size += ProtoSize::calc_bool(1, this->disabled_by_default); #ifdef USE_ENTITY_ICON - size += ProtoSize::calc_length(1, this->icon.size()); + size += this->icon.size() ? 2 + this->icon.size() : 0; #endif size += ProtoSize::calc_uint32(1, static_cast(this->entity_category)); #ifdef USE_DEVICES @@ -295,7 +295,7 @@ uint32_t ListEntitiesCoverResponse::calculate_size() const { size += ProtoSize::calc_length(1, this->device_class.size()); size += ProtoSize::calc_bool(1, this->disabled_by_default); #ifdef USE_ENTITY_ICON - size += ProtoSize::calc_length(1, this->icon.size()); + size += this->icon.size() ? 2 + this->icon.size() : 0; #endif size += ProtoSize::calc_uint32(1, static_cast(this->entity_category)); size += ProtoSize::calc_bool(1, this->supports_stop); @@ -398,7 +398,7 @@ uint32_t ListEntitiesFanResponse::calculate_size() const { size += ProtoSize::calc_int32(1, this->supported_speed_count); size += ProtoSize::calc_bool(1, this->disabled_by_default); #ifdef USE_ENTITY_ICON - size += ProtoSize::calc_length(1, this->icon.size()); + size += this->icon.size() ? 2 + this->icon.size() : 0; #endif size += ProtoSize::calc_uint32(1, static_cast(this->entity_category)); if (!this->supported_preset_modes->empty()) { @@ -541,7 +541,7 @@ uint32_t ListEntitiesLightResponse::calculate_size() const { } size += ProtoSize::calc_bool(1, this->disabled_by_default); #ifdef USE_ENTITY_ICON - size += ProtoSize::calc_length(1, this->icon.size()); + size += this->icon.size() ? 2 + this->icon.size() : 0; #endif size += ProtoSize::calc_uint32(1, static_cast(this->entity_category)); #ifdef USE_DEVICES @@ -725,7 +725,7 @@ uint32_t ListEntitiesSensorResponse::calculate_size() const { size += 5; size += 2 + this->name.size(); #ifdef USE_ENTITY_ICON - size += ProtoSize::calc_length(1, this->icon.size()); + size += this->icon.size() ? 2 + this->icon.size() : 0; #endif size += ProtoSize::calc_length(1, this->unit_of_measurement.size()); size += ProtoSize::calc_int32(1, this->accuracy_decimals); @@ -784,7 +784,7 @@ uint32_t ListEntitiesSwitchResponse::calculate_size() const { size += 5; size += 2 + this->name.size(); #ifdef USE_ENTITY_ICON - size += ProtoSize::calc_length(1, this->icon.size()); + size += this->icon.size() ? 2 + this->icon.size() : 0; #endif size += ProtoSize::calc_bool(1, this->assumed_state); size += ProtoSize::calc_bool(1, this->disabled_by_default); @@ -862,7 +862,7 @@ uint32_t ListEntitiesTextSensorResponse::calculate_size() const { size += 5; size += 2 + this->name.size(); #ifdef USE_ENTITY_ICON - size += ProtoSize::calc_length(1, this->icon.size()); + size += this->icon.size() ? 2 + this->icon.size() : 0; #endif size += ProtoSize::calc_bool(1, this->disabled_by_default); size += ProtoSize::calc_uint32(1, static_cast(this->entity_category)); @@ -1319,7 +1319,7 @@ uint32_t ListEntitiesCameraResponse::calculate_size() const { size += 2 + this->name.size(); size += ProtoSize::calc_bool(1, this->disabled_by_default); #ifdef USE_ENTITY_ICON - size += ProtoSize::calc_length(1, this->icon.size()); + size += this->icon.size() ? 2 + this->icon.size() : 0; #endif size += ProtoSize::calc_uint32(1, static_cast(this->entity_category)); #ifdef USE_DEVICES @@ -1450,7 +1450,7 @@ uint32_t ListEntitiesClimateResponse::calculate_size() const { } size += ProtoSize::calc_bool(2, this->disabled_by_default); #ifdef USE_ENTITY_ICON - size += ProtoSize::calc_length(2, this->icon.size()); + size += this->icon.size() ? 3 + this->icon.size() : 0; #endif size += ProtoSize::calc_uint32(2, static_cast(this->entity_category)); size += ProtoSize::calc_float(2, this->visual_current_temperature_step); @@ -1627,7 +1627,7 @@ uint32_t ListEntitiesWaterHeaterResponse::calculate_size() const { size += 5; size += 2 + this->name.size(); #ifdef USE_ENTITY_ICON - size += ProtoSize::calc_length(1, this->icon.size()); + size += this->icon.size() ? 2 + this->icon.size() : 0; #endif size += ProtoSize::calc_bool(1, this->disabled_by_default); size += ProtoSize::calc_uint32(1, static_cast(this->entity_category)); @@ -1742,7 +1742,7 @@ uint32_t ListEntitiesNumberResponse::calculate_size() const { size += 5; size += 2 + this->name.size(); #ifdef USE_ENTITY_ICON - size += ProtoSize::calc_length(1, this->icon.size()); + size += this->icon.size() ? 2 + this->icon.size() : 0; #endif size += ProtoSize::calc_float(1, this->min_value); size += ProtoSize::calc_float(1, this->max_value); @@ -1828,7 +1828,7 @@ uint32_t ListEntitiesSelectResponse::calculate_size() const { size += 5; size += 2 + this->name.size(); #ifdef USE_ENTITY_ICON - size += ProtoSize::calc_length(1, this->icon.size()); + size += this->icon.size() ? 2 + this->icon.size() : 0; #endif if (!this->options->empty()) { for (const char *it : *this->options) { @@ -1923,7 +1923,7 @@ uint32_t ListEntitiesSirenResponse::calculate_size() const { size += 5; size += 2 + this->name.size(); #ifdef USE_ENTITY_ICON - size += ProtoSize::calc_length(1, this->icon.size()); + size += this->icon.size() ? 2 + this->icon.size() : 0; #endif size += ProtoSize::calc_bool(1, this->disabled_by_default); if (!this->tones->empty()) { @@ -2038,7 +2038,7 @@ uint32_t ListEntitiesLockResponse::calculate_size() const { size += 5; size += 2 + this->name.size(); #ifdef USE_ENTITY_ICON - size += ProtoSize::calc_length(1, this->icon.size()); + size += this->icon.size() ? 2 + this->icon.size() : 0; #endif size += ProtoSize::calc_bool(1, this->disabled_by_default); size += ProtoSize::calc_uint32(1, static_cast(this->entity_category)); @@ -2132,7 +2132,7 @@ uint32_t ListEntitiesButtonResponse::calculate_size() const { size += 5; size += 2 + this->name.size(); #ifdef USE_ENTITY_ICON - size += ProtoSize::calc_length(1, this->icon.size()); + size += this->icon.size() ? 2 + this->icon.size() : 0; #endif size += ProtoSize::calc_bool(1, this->disabled_by_default); size += ProtoSize::calc_uint32(1, static_cast(this->entity_category)); @@ -2210,7 +2210,7 @@ uint32_t ListEntitiesMediaPlayerResponse::calculate_size() const { size += 5; size += 2 + this->name.size(); #ifdef USE_ENTITY_ICON - size += ProtoSize::calc_length(1, this->icon.size()); + size += this->icon.size() ? 2 + this->icon.size() : 0; #endif size += ProtoSize::calc_bool(1, this->disabled_by_default); size += ProtoSize::calc_uint32(1, static_cast(this->entity_category)); @@ -3034,7 +3034,7 @@ uint32_t ListEntitiesAlarmControlPanelResponse::calculate_size() const { size += 5; size += 2 + this->name.size(); #ifdef USE_ENTITY_ICON - size += ProtoSize::calc_length(1, this->icon.size()); + size += this->icon.size() ? 2 + this->icon.size() : 0; #endif size += ProtoSize::calc_bool(1, this->disabled_by_default); size += ProtoSize::calc_uint32(1, static_cast(this->entity_category)); @@ -3127,7 +3127,7 @@ uint32_t ListEntitiesTextResponse::calculate_size() const { size += 5; size += 2 + this->name.size(); #ifdef USE_ENTITY_ICON - size += ProtoSize::calc_length(1, this->icon.size()); + size += this->icon.size() ? 2 + this->icon.size() : 0; #endif size += ProtoSize::calc_bool(1, this->disabled_by_default); size += ProtoSize::calc_uint32(1, static_cast(this->entity_category)); @@ -3216,7 +3216,7 @@ uint32_t ListEntitiesDateResponse::calculate_size() const { size += 5; size += 2 + this->name.size(); #ifdef USE_ENTITY_ICON - size += ProtoSize::calc_length(1, this->icon.size()); + size += this->icon.size() ? 2 + this->icon.size() : 0; #endif size += ProtoSize::calc_bool(1, this->disabled_by_default); size += ProtoSize::calc_uint32(1, static_cast(this->entity_category)); @@ -3303,7 +3303,7 @@ uint32_t ListEntitiesTimeResponse::calculate_size() const { size += 5; size += 2 + this->name.size(); #ifdef USE_ENTITY_ICON - size += ProtoSize::calc_length(1, this->icon.size()); + size += this->icon.size() ? 2 + this->icon.size() : 0; #endif size += ProtoSize::calc_bool(1, this->disabled_by_default); size += ProtoSize::calc_uint32(1, static_cast(this->entity_category)); @@ -3394,7 +3394,7 @@ uint32_t ListEntitiesEventResponse::calculate_size() const { size += 5; size += 2 + this->name.size(); #ifdef USE_ENTITY_ICON - size += ProtoSize::calc_length(1, this->icon.size()); + size += this->icon.size() ? 2 + this->icon.size() : 0; #endif size += ProtoSize::calc_bool(1, this->disabled_by_default); size += ProtoSize::calc_uint32(1, static_cast(this->entity_category)); @@ -3454,7 +3454,7 @@ uint32_t ListEntitiesValveResponse::calculate_size() const { size += 5; size += 2 + this->name.size(); #ifdef USE_ENTITY_ICON - size += ProtoSize::calc_length(1, this->icon.size()); + size += this->icon.size() ? 2 + this->icon.size() : 0; #endif size += ProtoSize::calc_bool(1, this->disabled_by_default); size += ProtoSize::calc_uint32(1, static_cast(this->entity_category)); @@ -3541,7 +3541,7 @@ uint32_t ListEntitiesDateTimeResponse::calculate_size() const { size += 5; size += 2 + this->name.size(); #ifdef USE_ENTITY_ICON - size += ProtoSize::calc_length(1, this->icon.size()); + size += this->icon.size() ? 2 + this->icon.size() : 0; #endif size += ProtoSize::calc_bool(1, this->disabled_by_default); size += ProtoSize::calc_uint32(1, static_cast(this->entity_category)); @@ -3619,7 +3619,7 @@ uint32_t ListEntitiesUpdateResponse::calculate_size() const { size += 5; size += 2 + this->name.size(); #ifdef USE_ENTITY_ICON - size += ProtoSize::calc_length(1, this->icon.size()); + size += this->icon.size() ? 2 + this->icon.size() : 0; #endif size += ProtoSize::calc_bool(1, this->disabled_by_default); size += ProtoSize::calc_uint32(1, static_cast(this->entity_category)); @@ -3765,7 +3765,7 @@ uint32_t ListEntitiesInfraredResponse::calculate_size() const { size += 5; size += 2 + this->name.size(); #ifdef USE_ENTITY_ICON - size += ProtoSize::calc_length(1, this->icon.size()); + size += this->icon.size() ? 2 + this->icon.size() : 0; #endif size += ProtoSize::calc_bool(1, this->disabled_by_default); size += ProtoSize::calc_uint32(1, static_cast(this->entity_category)); From 675400cc314c5f7574c8770503f3f60dd439d39b Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 3 Apr 2026 13:51:25 -1000 Subject: [PATCH 19/31] [api] Use .empty() for string zero check in single-byte varint size --- esphome/components/api/api_pb2.cpp | 50 ++++++++++++++--------------- script/api_protobuf/api_protobuf.py | 15 +++++++-- 2 files changed, 37 insertions(+), 28 deletions(-) diff --git a/esphome/components/api/api_pb2.cpp b/esphome/components/api/api_pb2.cpp index 6ef7ca4d3f..9ba24449d7 100644 --- a/esphome/components/api/api_pb2.cpp +++ b/esphome/components/api/api_pb2.cpp @@ -234,7 +234,7 @@ uint32_t ListEntitiesBinarySensorResponse::calculate_size() const { size += ProtoSize::calc_bool(1, this->is_status_binary_sensor); size += ProtoSize::calc_bool(1, this->disabled_by_default); #ifdef USE_ENTITY_ICON - size += this->icon.size() ? 2 + this->icon.size() : 0; + size += !this->icon.empty() ? 2 + this->icon.size() : 0; #endif size += ProtoSize::calc_uint32(1, static_cast(this->entity_category)); #ifdef USE_DEVICES @@ -295,7 +295,7 @@ uint32_t ListEntitiesCoverResponse::calculate_size() const { size += ProtoSize::calc_length(1, this->device_class.size()); size += ProtoSize::calc_bool(1, this->disabled_by_default); #ifdef USE_ENTITY_ICON - size += this->icon.size() ? 2 + this->icon.size() : 0; + size += !this->icon.empty() ? 2 + this->icon.size() : 0; #endif size += ProtoSize::calc_uint32(1, static_cast(this->entity_category)); size += ProtoSize::calc_bool(1, this->supports_stop); @@ -398,7 +398,7 @@ uint32_t ListEntitiesFanResponse::calculate_size() const { size += ProtoSize::calc_int32(1, this->supported_speed_count); size += ProtoSize::calc_bool(1, this->disabled_by_default); #ifdef USE_ENTITY_ICON - size += this->icon.size() ? 2 + this->icon.size() : 0; + size += !this->icon.empty() ? 2 + this->icon.size() : 0; #endif size += ProtoSize::calc_uint32(1, static_cast(this->entity_category)); if (!this->supported_preset_modes->empty()) { @@ -541,7 +541,7 @@ uint32_t ListEntitiesLightResponse::calculate_size() const { } size += ProtoSize::calc_bool(1, this->disabled_by_default); #ifdef USE_ENTITY_ICON - size += this->icon.size() ? 2 + this->icon.size() : 0; + size += !this->icon.empty() ? 2 + this->icon.size() : 0; #endif size += ProtoSize::calc_uint32(1, static_cast(this->entity_category)); #ifdef USE_DEVICES @@ -725,7 +725,7 @@ uint32_t ListEntitiesSensorResponse::calculate_size() const { size += 5; size += 2 + this->name.size(); #ifdef USE_ENTITY_ICON - size += this->icon.size() ? 2 + this->icon.size() : 0; + size += !this->icon.empty() ? 2 + this->icon.size() : 0; #endif size += ProtoSize::calc_length(1, this->unit_of_measurement.size()); size += ProtoSize::calc_int32(1, this->accuracy_decimals); @@ -784,7 +784,7 @@ uint32_t ListEntitiesSwitchResponse::calculate_size() const { size += 5; size += 2 + this->name.size(); #ifdef USE_ENTITY_ICON - size += this->icon.size() ? 2 + this->icon.size() : 0; + size += !this->icon.empty() ? 2 + this->icon.size() : 0; #endif size += ProtoSize::calc_bool(1, this->assumed_state); size += ProtoSize::calc_bool(1, this->disabled_by_default); @@ -862,7 +862,7 @@ uint32_t ListEntitiesTextSensorResponse::calculate_size() const { size += 5; size += 2 + this->name.size(); #ifdef USE_ENTITY_ICON - size += this->icon.size() ? 2 + this->icon.size() : 0; + size += !this->icon.empty() ? 2 + this->icon.size() : 0; #endif size += ProtoSize::calc_bool(1, this->disabled_by_default); size += ProtoSize::calc_uint32(1, static_cast(this->entity_category)); @@ -1319,7 +1319,7 @@ uint32_t ListEntitiesCameraResponse::calculate_size() const { size += 2 + this->name.size(); size += ProtoSize::calc_bool(1, this->disabled_by_default); #ifdef USE_ENTITY_ICON - size += this->icon.size() ? 2 + this->icon.size() : 0; + size += !this->icon.empty() ? 2 + this->icon.size() : 0; #endif size += ProtoSize::calc_uint32(1, static_cast(this->entity_category)); #ifdef USE_DEVICES @@ -1450,7 +1450,7 @@ uint32_t ListEntitiesClimateResponse::calculate_size() const { } size += ProtoSize::calc_bool(2, this->disabled_by_default); #ifdef USE_ENTITY_ICON - size += this->icon.size() ? 3 + this->icon.size() : 0; + size += !this->icon.empty() ? 3 + this->icon.size() : 0; #endif size += ProtoSize::calc_uint32(2, static_cast(this->entity_category)); size += ProtoSize::calc_float(2, this->visual_current_temperature_step); @@ -1627,7 +1627,7 @@ uint32_t ListEntitiesWaterHeaterResponse::calculate_size() const { size += 5; size += 2 + this->name.size(); #ifdef USE_ENTITY_ICON - size += this->icon.size() ? 2 + this->icon.size() : 0; + size += !this->icon.empty() ? 2 + this->icon.size() : 0; #endif size += ProtoSize::calc_bool(1, this->disabled_by_default); size += ProtoSize::calc_uint32(1, static_cast(this->entity_category)); @@ -1742,7 +1742,7 @@ uint32_t ListEntitiesNumberResponse::calculate_size() const { size += 5; size += 2 + this->name.size(); #ifdef USE_ENTITY_ICON - size += this->icon.size() ? 2 + this->icon.size() : 0; + size += !this->icon.empty() ? 2 + this->icon.size() : 0; #endif size += ProtoSize::calc_float(1, this->min_value); size += ProtoSize::calc_float(1, this->max_value); @@ -1828,7 +1828,7 @@ uint32_t ListEntitiesSelectResponse::calculate_size() const { size += 5; size += 2 + this->name.size(); #ifdef USE_ENTITY_ICON - size += this->icon.size() ? 2 + this->icon.size() : 0; + size += !this->icon.empty() ? 2 + this->icon.size() : 0; #endif if (!this->options->empty()) { for (const char *it : *this->options) { @@ -1923,7 +1923,7 @@ uint32_t ListEntitiesSirenResponse::calculate_size() const { size += 5; size += 2 + this->name.size(); #ifdef USE_ENTITY_ICON - size += this->icon.size() ? 2 + this->icon.size() : 0; + size += !this->icon.empty() ? 2 + this->icon.size() : 0; #endif size += ProtoSize::calc_bool(1, this->disabled_by_default); if (!this->tones->empty()) { @@ -2038,7 +2038,7 @@ uint32_t ListEntitiesLockResponse::calculate_size() const { size += 5; size += 2 + this->name.size(); #ifdef USE_ENTITY_ICON - size += this->icon.size() ? 2 + this->icon.size() : 0; + size += !this->icon.empty() ? 2 + this->icon.size() : 0; #endif size += ProtoSize::calc_bool(1, this->disabled_by_default); size += ProtoSize::calc_uint32(1, static_cast(this->entity_category)); @@ -2132,7 +2132,7 @@ uint32_t ListEntitiesButtonResponse::calculate_size() const { size += 5; size += 2 + this->name.size(); #ifdef USE_ENTITY_ICON - size += this->icon.size() ? 2 + this->icon.size() : 0; + size += !this->icon.empty() ? 2 + this->icon.size() : 0; #endif size += ProtoSize::calc_bool(1, this->disabled_by_default); size += ProtoSize::calc_uint32(1, static_cast(this->entity_category)); @@ -2210,7 +2210,7 @@ uint32_t ListEntitiesMediaPlayerResponse::calculate_size() const { size += 5; size += 2 + this->name.size(); #ifdef USE_ENTITY_ICON - size += this->icon.size() ? 2 + this->icon.size() : 0; + size += !this->icon.empty() ? 2 + this->icon.size() : 0; #endif size += ProtoSize::calc_bool(1, this->disabled_by_default); size += ProtoSize::calc_uint32(1, static_cast(this->entity_category)); @@ -3034,7 +3034,7 @@ uint32_t ListEntitiesAlarmControlPanelResponse::calculate_size() const { size += 5; size += 2 + this->name.size(); #ifdef USE_ENTITY_ICON - size += this->icon.size() ? 2 + this->icon.size() : 0; + size += !this->icon.empty() ? 2 + this->icon.size() : 0; #endif size += ProtoSize::calc_bool(1, this->disabled_by_default); size += ProtoSize::calc_uint32(1, static_cast(this->entity_category)); @@ -3127,7 +3127,7 @@ uint32_t ListEntitiesTextResponse::calculate_size() const { size += 5; size += 2 + this->name.size(); #ifdef USE_ENTITY_ICON - size += this->icon.size() ? 2 + this->icon.size() : 0; + size += !this->icon.empty() ? 2 + this->icon.size() : 0; #endif size += ProtoSize::calc_bool(1, this->disabled_by_default); size += ProtoSize::calc_uint32(1, static_cast(this->entity_category)); @@ -3216,7 +3216,7 @@ uint32_t ListEntitiesDateResponse::calculate_size() const { size += 5; size += 2 + this->name.size(); #ifdef USE_ENTITY_ICON - size += this->icon.size() ? 2 + this->icon.size() : 0; + size += !this->icon.empty() ? 2 + this->icon.size() : 0; #endif size += ProtoSize::calc_bool(1, this->disabled_by_default); size += ProtoSize::calc_uint32(1, static_cast(this->entity_category)); @@ -3303,7 +3303,7 @@ uint32_t ListEntitiesTimeResponse::calculate_size() const { size += 5; size += 2 + this->name.size(); #ifdef USE_ENTITY_ICON - size += this->icon.size() ? 2 + this->icon.size() : 0; + size += !this->icon.empty() ? 2 + this->icon.size() : 0; #endif size += ProtoSize::calc_bool(1, this->disabled_by_default); size += ProtoSize::calc_uint32(1, static_cast(this->entity_category)); @@ -3394,7 +3394,7 @@ uint32_t ListEntitiesEventResponse::calculate_size() const { size += 5; size += 2 + this->name.size(); #ifdef USE_ENTITY_ICON - size += this->icon.size() ? 2 + this->icon.size() : 0; + size += !this->icon.empty() ? 2 + this->icon.size() : 0; #endif size += ProtoSize::calc_bool(1, this->disabled_by_default); size += ProtoSize::calc_uint32(1, static_cast(this->entity_category)); @@ -3454,7 +3454,7 @@ uint32_t ListEntitiesValveResponse::calculate_size() const { size += 5; size += 2 + this->name.size(); #ifdef USE_ENTITY_ICON - size += this->icon.size() ? 2 + this->icon.size() : 0; + size += !this->icon.empty() ? 2 + this->icon.size() : 0; #endif size += ProtoSize::calc_bool(1, this->disabled_by_default); size += ProtoSize::calc_uint32(1, static_cast(this->entity_category)); @@ -3541,7 +3541,7 @@ uint32_t ListEntitiesDateTimeResponse::calculate_size() const { size += 5; size += 2 + this->name.size(); #ifdef USE_ENTITY_ICON - size += this->icon.size() ? 2 + this->icon.size() : 0; + size += !this->icon.empty() ? 2 + this->icon.size() : 0; #endif size += ProtoSize::calc_bool(1, this->disabled_by_default); size += ProtoSize::calc_uint32(1, static_cast(this->entity_category)); @@ -3619,7 +3619,7 @@ uint32_t ListEntitiesUpdateResponse::calculate_size() const { size += 5; size += 2 + this->name.size(); #ifdef USE_ENTITY_ICON - size += this->icon.size() ? 2 + this->icon.size() : 0; + size += !this->icon.empty() ? 2 + this->icon.size() : 0; #endif size += ProtoSize::calc_bool(1, this->disabled_by_default); size += ProtoSize::calc_uint32(1, static_cast(this->entity_category)); @@ -3765,7 +3765,7 @@ uint32_t ListEntitiesInfraredResponse::calculate_size() const { size += 5; size += 2 + this->name.size(); #ifdef USE_ENTITY_ICON - size += this->icon.size() ? 2 + this->icon.size() : 0; + size += !this->icon.empty() ? 2 + this->icon.size() : 0; #endif size += ProtoSize::calc_bool(1, this->disabled_by_default); size += ProtoSize::calc_uint32(1, static_cast(this->entity_category)); diff --git a/script/api_protobuf/api_protobuf.py b/script/api_protobuf/api_protobuf.py index a0335ff404..208412b62b 100755 --- a/script/api_protobuf/api_protobuf.py +++ b/script/api_protobuf/api_protobuf.py @@ -376,7 +376,11 @@ class TypeInfo(ABC): return f"size += ProtoSize::{method}({field_id_size}, {value});" def _get_single_byte_varint_size( - self, name: str, force: bool, extra_expr: str | None = None + self, + name: str, + force: bool, + extra_expr: str | None = None, + zero_check: str | None = None, ) -> str: """Size calculation when the varint is guaranteed to be 1 byte. @@ -387,12 +391,14 @@ class TypeInfo(ABC): name: Expression to check for zero (non-force only) force: Whether to skip the zero check extra_expr: Additional variable expression to add (e.g., data length) + zero_check: Override expression for the zero check (e.g., "!x.empty()") """ fixed = self.calculate_field_id_size() + 1 size_expr = f"{fixed} + {extra_expr}" if extra_expr else str(fixed) if force: return f"size += {size_expr};" - return f"size += {name} ? {size_expr} : 0;" + check = zero_check or name + return f"size += {check} ? {size_expr} : 0;" @abstractmethod def get_size_calculation(self, name: str, force: bool = False) -> str: @@ -1100,7 +1106,10 @@ class PointerToStringBufferType(PointerToBufferTypeBase): max_len = self.max_data_length if max_len is not None and max_len < 128: return self._get_single_byte_varint_size( - size_field, force, extra_expr=size_field + size_field, + force, + extra_expr=size_field, + zero_check=f"!this->{self.field_name}.empty()", ) return self._get_simple_size_calculation(size_field, force, "length") From 2d13def0eee1c3a1af6c2902583355e1bb04bc75 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 3 Apr 2026 14:00:01 -1000 Subject: [PATCH 20/31] [api] Add encode_raw_short_string for forced string fields with max_data_length Replace 3-call sequence (write_raw_byte + write_raw_byte + encode_raw) with single inlined encode_raw_short_string call for forced string fields with max_data_length < 128. The compiler can hoist the pos pointer across all three writes in one function boundary. --- esphome/components/api/api_pb2.cpp | 200 +++++++--------------------- esphome/components/api/proto.h | 10 ++ script/api_protobuf/api_protobuf.py | 8 +- 3 files changed, 67 insertions(+), 151 deletions(-) diff --git a/esphome/components/api/api_pb2.cpp b/esphome/components/api/api_pb2.cpp index 9ba24449d7..565d86b41b 100644 --- a/esphome/components/api/api_pb2.cpp +++ b/esphome/components/api/api_pb2.cpp @@ -207,13 +207,9 @@ uint32_t DeviceInfoResponse::calculate_size() const { } #ifdef USE_BINARY_SENSOR void ListEntitiesBinarySensorResponse::encode(ProtoWriteBuffer &buffer) const { - buffer.write_raw_byte(10); - buffer.write_raw_byte(static_cast(this->object_id.size())); - buffer.encode_raw(this->object_id.c_str(), this->object_id.size()); + buffer.encode_raw_short_string(10, this->object_id); buffer.write_tag_and_fixed32(21, this->key); - buffer.write_raw_byte(26); - buffer.write_raw_byte(static_cast(this->name.size())); - buffer.encode_raw(this->name.c_str(), this->name.size()); + buffer.encode_raw_short_string(26, this->name); buffer.encode_string(5, this->device_class); buffer.encode_bool(6, this->is_status_binary_sensor); buffer.encode_bool(7, this->disabled_by_default); @@ -263,13 +259,9 @@ uint32_t BinarySensorStateResponse::calculate_size() const { #endif #ifdef USE_COVER void ListEntitiesCoverResponse::encode(ProtoWriteBuffer &buffer) const { - buffer.write_raw_byte(10); - buffer.write_raw_byte(static_cast(this->object_id.size())); - buffer.encode_raw(this->object_id.c_str(), this->object_id.size()); + buffer.encode_raw_short_string(10, this->object_id); buffer.write_tag_and_fixed32(21, this->key); - buffer.write_raw_byte(26); - buffer.write_raw_byte(static_cast(this->name.size())); - buffer.encode_raw(this->name.c_str(), this->name.size()); + buffer.encode_raw_short_string(26, this->name); buffer.encode_bool(5, this->assumed_state); buffer.encode_bool(6, this->supports_position); buffer.encode_bool(7, this->supports_tilt); @@ -364,13 +356,9 @@ bool CoverCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) { #endif #ifdef USE_FAN void ListEntitiesFanResponse::encode(ProtoWriteBuffer &buffer) const { - buffer.write_raw_byte(10); - buffer.write_raw_byte(static_cast(this->object_id.size())); - buffer.encode_raw(this->object_id.c_str(), this->object_id.size()); + buffer.encode_raw_short_string(10, this->object_id); buffer.write_tag_and_fixed32(21, this->key); - buffer.write_raw_byte(26); - buffer.write_raw_byte(static_cast(this->name.size())); - buffer.encode_raw(this->name.c_str(), this->name.size()); + buffer.encode_raw_short_string(26, this->name); buffer.encode_bool(5, this->supports_oscillation); buffer.encode_bool(6, this->supports_speed); buffer.encode_bool(7, this->supports_direction); @@ -498,13 +486,9 @@ bool FanCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) { #endif #ifdef USE_LIGHT void ListEntitiesLightResponse::encode(ProtoWriteBuffer &buffer) const { - buffer.write_raw_byte(10); - buffer.write_raw_byte(static_cast(this->object_id.size())); - buffer.encode_raw(this->object_id.c_str(), this->object_id.size()); + buffer.encode_raw_short_string(10, this->object_id); buffer.write_tag_and_fixed32(21, this->key); - buffer.write_raw_byte(26); - buffer.write_raw_byte(static_cast(this->name.size())); - buffer.encode_raw(this->name.c_str(), this->name.size()); + buffer.encode_raw_short_string(26, this->name); for (const auto &it : *this->supported_color_modes) { buffer.encode_uint32(12, static_cast(it), true); } @@ -698,13 +682,9 @@ bool LightCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) { #endif #ifdef USE_SENSOR void ListEntitiesSensorResponse::encode(ProtoWriteBuffer &buffer) const { - buffer.write_raw_byte(10); - buffer.write_raw_byte(static_cast(this->object_id.size())); - buffer.encode_raw(this->object_id.c_str(), this->object_id.size()); + buffer.encode_raw_short_string(10, this->object_id); buffer.write_tag_and_fixed32(21, this->key); - buffer.write_raw_byte(26); - buffer.write_raw_byte(static_cast(this->name.size())); - buffer.encode_raw(this->name.c_str(), this->name.size()); + buffer.encode_raw_short_string(26, this->name); #ifdef USE_ENTITY_ICON buffer.encode_string(5, this->icon); #endif @@ -760,13 +740,9 @@ uint32_t SensorStateResponse::calculate_size() const { #endif #ifdef USE_SWITCH void ListEntitiesSwitchResponse::encode(ProtoWriteBuffer &buffer) const { - buffer.write_raw_byte(10); - buffer.write_raw_byte(static_cast(this->object_id.size())); - buffer.encode_raw(this->object_id.c_str(), this->object_id.size()); + buffer.encode_raw_short_string(10, this->object_id); buffer.write_tag_and_fixed32(21, this->key); - buffer.write_raw_byte(26); - buffer.write_raw_byte(static_cast(this->name.size())); - buffer.encode_raw(this->name.c_str(), this->name.size()); + buffer.encode_raw_short_string(26, this->name); #ifdef USE_ENTITY_ICON buffer.encode_string(5, this->icon); #endif @@ -839,13 +815,9 @@ bool SwitchCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) { #endif #ifdef USE_TEXT_SENSOR void ListEntitiesTextSensorResponse::encode(ProtoWriteBuffer &buffer) const { - buffer.write_raw_byte(10); - buffer.write_raw_byte(static_cast(this->object_id.size())); - buffer.encode_raw(this->object_id.c_str(), this->object_id.size()); + buffer.encode_raw_short_string(10, this->object_id); buffer.write_tag_and_fixed32(21, this->key); - buffer.write_raw_byte(26); - buffer.write_raw_byte(static_cast(this->name.size())); - buffer.encode_raw(this->name.c_str(), this->name.size()); + buffer.encode_raw_short_string(26, this->name); #ifdef USE_ENTITY_ICON buffer.encode_string(5, this->icon); #endif @@ -1296,13 +1268,9 @@ uint32_t ExecuteServiceResponse::calculate_size() const { #endif #ifdef USE_CAMERA void ListEntitiesCameraResponse::encode(ProtoWriteBuffer &buffer) const { - buffer.write_raw_byte(10); - buffer.write_raw_byte(static_cast(this->object_id.size())); - buffer.encode_raw(this->object_id.c_str(), this->object_id.size()); + buffer.encode_raw_short_string(10, this->object_id); buffer.write_tag_and_fixed32(21, this->key); - buffer.write_raw_byte(26); - buffer.write_raw_byte(static_cast(this->name.size())); - buffer.encode_raw(this->name.c_str(), this->name.size()); + buffer.encode_raw_short_string(26, this->name); buffer.encode_bool(5, this->disabled_by_default); #ifdef USE_ENTITY_ICON buffer.encode_string(6, this->icon); @@ -1361,13 +1329,9 @@ bool CameraImageRequest::decode_varint(uint32_t field_id, proto_varint_value_t v #endif #ifdef USE_CLIMATE void ListEntitiesClimateResponse::encode(ProtoWriteBuffer &buffer) const { - buffer.write_raw_byte(10); - buffer.write_raw_byte(static_cast(this->object_id.size())); - buffer.encode_raw(this->object_id.c_str(), this->object_id.size()); + buffer.encode_raw_short_string(10, this->object_id); buffer.write_tag_and_fixed32(21, this->key); - buffer.write_raw_byte(26); - buffer.write_raw_byte(static_cast(this->name.size())); - buffer.encode_raw(this->name.c_str(), this->name.size()); + buffer.encode_raw_short_string(26, this->name); buffer.encode_bool(5, this->supports_current_temperature); buffer.encode_bool(6, this->supports_two_point_target_temperature); for (const auto &it : *this->supported_modes) { @@ -1598,13 +1562,9 @@ bool ClimateCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) { #endif #ifdef USE_WATER_HEATER void ListEntitiesWaterHeaterResponse::encode(ProtoWriteBuffer &buffer) const { - buffer.write_raw_byte(10); - buffer.write_raw_byte(static_cast(this->object_id.size())); - buffer.encode_raw(this->object_id.c_str(), this->object_id.size()); + buffer.encode_raw_short_string(10, this->object_id); buffer.write_tag_and_fixed32(21, this->key); - buffer.write_raw_byte(26); - buffer.write_raw_byte(static_cast(this->name.size())); - buffer.encode_raw(this->name.c_str(), this->name.size()); + buffer.encode_raw_short_string(26, this->name); #ifdef USE_ENTITY_ICON buffer.encode_string(4, this->icon); #endif @@ -1714,13 +1674,9 @@ bool WaterHeaterCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value #endif #ifdef USE_NUMBER void ListEntitiesNumberResponse::encode(ProtoWriteBuffer &buffer) const { - buffer.write_raw_byte(10); - buffer.write_raw_byte(static_cast(this->object_id.size())); - buffer.encode_raw(this->object_id.c_str(), this->object_id.size()); + buffer.encode_raw_short_string(10, this->object_id); buffer.write_tag_and_fixed32(21, this->key); - buffer.write_raw_byte(26); - buffer.write_raw_byte(static_cast(this->name.size())); - buffer.encode_raw(this->name.c_str(), this->name.size()); + buffer.encode_raw_short_string(26, this->name); #ifdef USE_ENTITY_ICON buffer.encode_string(5, this->icon); #endif @@ -1803,13 +1759,9 @@ bool NumberCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) { #endif #ifdef USE_SELECT void ListEntitiesSelectResponse::encode(ProtoWriteBuffer &buffer) const { - buffer.write_raw_byte(10); - buffer.write_raw_byte(static_cast(this->object_id.size())); - buffer.encode_raw(this->object_id.c_str(), this->object_id.size()); + buffer.encode_raw_short_string(10, this->object_id); buffer.write_tag_and_fixed32(21, this->key); - buffer.write_raw_byte(26); - buffer.write_raw_byte(static_cast(this->name.size())); - buffer.encode_raw(this->name.c_str(), this->name.size()); + buffer.encode_raw_short_string(26, this->name); #ifdef USE_ENTITY_ICON buffer.encode_string(5, this->icon); #endif @@ -1896,13 +1848,9 @@ bool SelectCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) { #endif #ifdef USE_SIREN void ListEntitiesSirenResponse::encode(ProtoWriteBuffer &buffer) const { - buffer.write_raw_byte(10); - buffer.write_raw_byte(static_cast(this->object_id.size())); - buffer.encode_raw(this->object_id.c_str(), this->object_id.size()); + buffer.encode_raw_short_string(10, this->object_id); buffer.write_tag_and_fixed32(21, this->key); - buffer.write_raw_byte(26); - buffer.write_raw_byte(static_cast(this->name.size())); - buffer.encode_raw(this->name.c_str(), this->name.size()); + buffer.encode_raw_short_string(26, this->name); #ifdef USE_ENTITY_ICON buffer.encode_string(5, this->icon); #endif @@ -2012,13 +1960,9 @@ bool SirenCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) { #endif #ifdef USE_LOCK void ListEntitiesLockResponse::encode(ProtoWriteBuffer &buffer) const { - buffer.write_raw_byte(10); - buffer.write_raw_byte(static_cast(this->object_id.size())); - buffer.encode_raw(this->object_id.c_str(), this->object_id.size()); + buffer.encode_raw_short_string(10, this->object_id); buffer.write_tag_and_fixed32(21, this->key); - buffer.write_raw_byte(26); - buffer.write_raw_byte(static_cast(this->name.size())); - buffer.encode_raw(this->name.c_str(), this->name.size()); + buffer.encode_raw_short_string(26, this->name); #ifdef USE_ENTITY_ICON buffer.encode_string(5, this->icon); #endif @@ -2109,13 +2053,9 @@ bool LockCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) { #endif #ifdef USE_BUTTON void ListEntitiesButtonResponse::encode(ProtoWriteBuffer &buffer) const { - buffer.write_raw_byte(10); - buffer.write_raw_byte(static_cast(this->object_id.size())); - buffer.encode_raw(this->object_id.c_str(), this->object_id.size()); + buffer.encode_raw_short_string(10, this->object_id); buffer.write_tag_and_fixed32(21, this->key); - buffer.write_raw_byte(26); - buffer.write_raw_byte(static_cast(this->name.size())); - buffer.encode_raw(this->name.c_str(), this->name.size()); + buffer.encode_raw_short_string(26, this->name); #ifdef USE_ENTITY_ICON buffer.encode_string(5, this->icon); #endif @@ -2183,13 +2123,9 @@ uint32_t MediaPlayerSupportedFormat::calculate_size() const { return size; } void ListEntitiesMediaPlayerResponse::encode(ProtoWriteBuffer &buffer) const { - buffer.write_raw_byte(10); - buffer.write_raw_byte(static_cast(this->object_id.size())); - buffer.encode_raw(this->object_id.c_str(), this->object_id.size()); + buffer.encode_raw_short_string(10, this->object_id); buffer.write_tag_and_fixed32(21, this->key); - buffer.write_raw_byte(26); - buffer.write_raw_byte(static_cast(this->name.size())); - buffer.encode_raw(this->name.c_str(), this->name.size()); + buffer.encode_raw_short_string(26, this->name); #ifdef USE_ENTITY_ICON buffer.encode_string(5, this->icon); #endif @@ -3009,13 +2945,9 @@ bool VoiceAssistantSetConfiguration::decode_length(uint32_t field_id, ProtoLengt #endif #ifdef USE_ALARM_CONTROL_PANEL void ListEntitiesAlarmControlPanelResponse::encode(ProtoWriteBuffer &buffer) const { - buffer.write_raw_byte(10); - buffer.write_raw_byte(static_cast(this->object_id.size())); - buffer.encode_raw(this->object_id.c_str(), this->object_id.size()); + buffer.encode_raw_short_string(10, this->object_id); buffer.write_tag_and_fixed32(21, this->key); - buffer.write_raw_byte(26); - buffer.write_raw_byte(static_cast(this->name.size())); - buffer.encode_raw(this->name.c_str(), this->name.size()); + buffer.encode_raw_short_string(26, this->name); #ifdef USE_ENTITY_ICON buffer.encode_string(5, this->icon); #endif @@ -3101,13 +3033,9 @@ bool AlarmControlPanelCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit #endif #ifdef USE_TEXT void ListEntitiesTextResponse::encode(ProtoWriteBuffer &buffer) const { - buffer.write_raw_byte(10); - buffer.write_raw_byte(static_cast(this->object_id.size())); - buffer.encode_raw(this->object_id.c_str(), this->object_id.size()); + buffer.encode_raw_short_string(10, this->object_id); buffer.write_tag_and_fixed32(21, this->key); - buffer.write_raw_byte(26); - buffer.write_raw_byte(static_cast(this->name.size())); - buffer.encode_raw(this->name.c_str(), this->name.size()); + buffer.encode_raw_short_string(26, this->name); #ifdef USE_ENTITY_ICON buffer.encode_string(5, this->icon); #endif @@ -3194,13 +3122,9 @@ bool TextCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) { #endif #ifdef USE_DATETIME_DATE void ListEntitiesDateResponse::encode(ProtoWriteBuffer &buffer) const { - buffer.write_raw_byte(10); - buffer.write_raw_byte(static_cast(this->object_id.size())); - buffer.encode_raw(this->object_id.c_str(), this->object_id.size()); + buffer.encode_raw_short_string(10, this->object_id); buffer.write_tag_and_fixed32(21, this->key); - buffer.write_raw_byte(26); - buffer.write_raw_byte(static_cast(this->name.size())); - buffer.encode_raw(this->name.c_str(), this->name.size()); + buffer.encode_raw_short_string(26, this->name); #ifdef USE_ENTITY_ICON buffer.encode_string(5, this->icon); #endif @@ -3281,13 +3205,9 @@ bool DateCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) { #endif #ifdef USE_DATETIME_TIME void ListEntitiesTimeResponse::encode(ProtoWriteBuffer &buffer) const { - buffer.write_raw_byte(10); - buffer.write_raw_byte(static_cast(this->object_id.size())); - buffer.encode_raw(this->object_id.c_str(), this->object_id.size()); + buffer.encode_raw_short_string(10, this->object_id); buffer.write_tag_and_fixed32(21, this->key); - buffer.write_raw_byte(26); - buffer.write_raw_byte(static_cast(this->name.size())); - buffer.encode_raw(this->name.c_str(), this->name.size()); + buffer.encode_raw_short_string(26, this->name); #ifdef USE_ENTITY_ICON buffer.encode_string(5, this->icon); #endif @@ -3368,13 +3288,9 @@ bool TimeCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) { #endif #ifdef USE_EVENT void ListEntitiesEventResponse::encode(ProtoWriteBuffer &buffer) const { - buffer.write_raw_byte(10); - buffer.write_raw_byte(static_cast(this->object_id.size())); - buffer.encode_raw(this->object_id.c_str(), this->object_id.size()); + buffer.encode_raw_short_string(10, this->object_id); buffer.write_tag_and_fixed32(21, this->key); - buffer.write_raw_byte(26); - buffer.write_raw_byte(static_cast(this->name.size())); - buffer.encode_raw(this->name.c_str(), this->name.size()); + buffer.encode_raw_short_string(26, this->name); #ifdef USE_ENTITY_ICON buffer.encode_string(5, this->icon); #endif @@ -3428,13 +3344,9 @@ uint32_t EventResponse::calculate_size() const { #endif #ifdef USE_VALVE void ListEntitiesValveResponse::encode(ProtoWriteBuffer &buffer) const { - buffer.write_raw_byte(10); - buffer.write_raw_byte(static_cast(this->object_id.size())); - buffer.encode_raw(this->object_id.c_str(), this->object_id.size()); + buffer.encode_raw_short_string(10, this->object_id); buffer.write_tag_and_fixed32(21, this->key); - buffer.write_raw_byte(26); - buffer.write_raw_byte(static_cast(this->name.size())); - buffer.encode_raw(this->name.c_str(), this->name.size()); + buffer.encode_raw_short_string(26, this->name); #ifdef USE_ENTITY_ICON buffer.encode_string(5, this->icon); #endif @@ -3519,13 +3431,9 @@ bool ValveCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) { #endif #ifdef USE_DATETIME_DATETIME void ListEntitiesDateTimeResponse::encode(ProtoWriteBuffer &buffer) const { - buffer.write_raw_byte(10); - buffer.write_raw_byte(static_cast(this->object_id.size())); - buffer.encode_raw(this->object_id.c_str(), this->object_id.size()); + buffer.encode_raw_short_string(10, this->object_id); buffer.write_tag_and_fixed32(21, this->key); - buffer.write_raw_byte(26); - buffer.write_raw_byte(static_cast(this->name.size())); - buffer.encode_raw(this->name.c_str(), this->name.size()); + buffer.encode_raw_short_string(26, this->name); #ifdef USE_ENTITY_ICON buffer.encode_string(5, this->icon); #endif @@ -3596,13 +3504,9 @@ bool DateTimeCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) { #endif #ifdef USE_UPDATE void ListEntitiesUpdateResponse::encode(ProtoWriteBuffer &buffer) const { - buffer.write_raw_byte(10); - buffer.write_raw_byte(static_cast(this->object_id.size())); - buffer.encode_raw(this->object_id.c_str(), this->object_id.size()); + buffer.encode_raw_short_string(10, this->object_id); buffer.write_tag_and_fixed32(21, this->key); - buffer.write_raw_byte(26); - buffer.write_raw_byte(static_cast(this->name.size())); - buffer.encode_raw(this->name.c_str(), this->name.size()); + buffer.encode_raw_short_string(26, this->name); #ifdef USE_ENTITY_ICON buffer.encode_string(5, this->icon); #endif @@ -3741,13 +3645,9 @@ uint32_t ZWaveProxyRequest::calculate_size() const { #endif #ifdef USE_INFRARED void ListEntitiesInfraredResponse::encode(ProtoWriteBuffer &buffer) const { - buffer.write_raw_byte(10); - buffer.write_raw_byte(static_cast(this->object_id.size())); - buffer.encode_raw(this->object_id.c_str(), this->object_id.size()); + buffer.encode_raw_short_string(10, this->object_id); buffer.write_tag_and_fixed32(21, this->key); - buffer.write_raw_byte(26); - buffer.write_raw_byte(static_cast(this->name.size())); - buffer.encode_raw(this->name.c_str(), this->name.size()); + buffer.encode_raw_short_string(26, this->name); #ifdef USE_ENTITY_ICON buffer.encode_string(4, this->icon); #endif diff --git a/esphome/components/api/proto.h b/esphome/components/api/proto.h index b629018a91..86f0085d32 100644 --- a/esphome/components/api/proto.h +++ b/esphome/components/api/proto.h @@ -240,6 +240,16 @@ class ProtoWriteBuffer { std::memcpy(this->pos_, data, len); this->pos_ += len; } + /// Write tag + 1-byte length + raw string data. For strings with max_data_length < 128. + /// Tag must be a single-byte varint (< 128). Always encodes (no zero check). + inline void encode_raw_short_string(uint8_t tag, const StringRef &ref) ESPHOME_ALWAYS_INLINE { + this->debug_check_bounds_(2 + ref.size()); + uint8_t *__restrict__ pos = this->pos_; + *pos++ = tag; + *pos++ = static_cast(ref.size()); + std::memcpy(pos, ref.c_str(), ref.size()); + this->pos_ = pos + ref.size(); + } /// Write a precomputed tag byte + 32-bit value in one operation. /// Tag must be a single-byte varint (< 128). No zero check. inline void write_tag_and_fixed32(uint8_t tag, uint32_t value) ESPHOME_ALWAYS_INLINE { diff --git a/script/api_protobuf/api_protobuf.py b/script/api_protobuf/api_protobuf.py index 208412b62b..644ff85b96 100755 --- a/script/api_protobuf/api_protobuf.py +++ b/script/api_protobuf/api_protobuf.py @@ -1074,10 +1074,16 @@ class PointerToStringBufferType(PointerToBufferTypeBase): @property def encode_content(self) -> str: + max_len = self.max_data_length + if max_len is not None and max_len < 128 and self.force: + tag = self.calculate_tag() + if tag < 128: + return ( + f"buffer.encode_raw_short_string({tag}, this->{self.field_name});" + ) if result := self._encode_bytes_with_precomputed_tag( f"this->{self.field_name}.c_str()", f"this->{self.field_name}.size()", - max_len=self.max_data_length, ): return result if self.force: From fd68e9a8274d544f9a9742ddfc4d052796801bdf Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 3 Apr 2026 14:01:14 -1000 Subject: [PATCH 21/31] [api] Rename encode_raw_short_string to encode_short_string --- esphome/components/api/api_pb2.cpp | 100 ++++++++++++++-------------- esphome/components/api/proto.h | 2 +- script/api_protobuf/api_protobuf.py | 4 +- 3 files changed, 52 insertions(+), 54 deletions(-) diff --git a/esphome/components/api/api_pb2.cpp b/esphome/components/api/api_pb2.cpp index 565d86b41b..8dea47bb6b 100644 --- a/esphome/components/api/api_pb2.cpp +++ b/esphome/components/api/api_pb2.cpp @@ -207,9 +207,9 @@ uint32_t DeviceInfoResponse::calculate_size() const { } #ifdef USE_BINARY_SENSOR void ListEntitiesBinarySensorResponse::encode(ProtoWriteBuffer &buffer) const { - buffer.encode_raw_short_string(10, this->object_id); + buffer.encode_short_string(10, this->object_id); buffer.write_tag_and_fixed32(21, this->key); - buffer.encode_raw_short_string(26, this->name); + buffer.encode_short_string(26, this->name); buffer.encode_string(5, this->device_class); buffer.encode_bool(6, this->is_status_binary_sensor); buffer.encode_bool(7, this->disabled_by_default); @@ -259,9 +259,9 @@ uint32_t BinarySensorStateResponse::calculate_size() const { #endif #ifdef USE_COVER void ListEntitiesCoverResponse::encode(ProtoWriteBuffer &buffer) const { - buffer.encode_raw_short_string(10, this->object_id); + buffer.encode_short_string(10, this->object_id); buffer.write_tag_and_fixed32(21, this->key); - buffer.encode_raw_short_string(26, this->name); + buffer.encode_short_string(26, this->name); buffer.encode_bool(5, this->assumed_state); buffer.encode_bool(6, this->supports_position); buffer.encode_bool(7, this->supports_tilt); @@ -356,9 +356,9 @@ bool CoverCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) { #endif #ifdef USE_FAN void ListEntitiesFanResponse::encode(ProtoWriteBuffer &buffer) const { - buffer.encode_raw_short_string(10, this->object_id); + buffer.encode_short_string(10, this->object_id); buffer.write_tag_and_fixed32(21, this->key); - buffer.encode_raw_short_string(26, this->name); + buffer.encode_short_string(26, this->name); buffer.encode_bool(5, this->supports_oscillation); buffer.encode_bool(6, this->supports_speed); buffer.encode_bool(7, this->supports_direction); @@ -486,9 +486,9 @@ bool FanCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) { #endif #ifdef USE_LIGHT void ListEntitiesLightResponse::encode(ProtoWriteBuffer &buffer) const { - buffer.encode_raw_short_string(10, this->object_id); + buffer.encode_short_string(10, this->object_id); buffer.write_tag_and_fixed32(21, this->key); - buffer.encode_raw_short_string(26, this->name); + buffer.encode_short_string(26, this->name); for (const auto &it : *this->supported_color_modes) { buffer.encode_uint32(12, static_cast(it), true); } @@ -682,9 +682,9 @@ bool LightCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) { #endif #ifdef USE_SENSOR void ListEntitiesSensorResponse::encode(ProtoWriteBuffer &buffer) const { - buffer.encode_raw_short_string(10, this->object_id); + buffer.encode_short_string(10, this->object_id); buffer.write_tag_and_fixed32(21, this->key); - buffer.encode_raw_short_string(26, this->name); + buffer.encode_short_string(26, this->name); #ifdef USE_ENTITY_ICON buffer.encode_string(5, this->icon); #endif @@ -740,9 +740,9 @@ uint32_t SensorStateResponse::calculate_size() const { #endif #ifdef USE_SWITCH void ListEntitiesSwitchResponse::encode(ProtoWriteBuffer &buffer) const { - buffer.encode_raw_short_string(10, this->object_id); + buffer.encode_short_string(10, this->object_id); buffer.write_tag_and_fixed32(21, this->key); - buffer.encode_raw_short_string(26, this->name); + buffer.encode_short_string(26, this->name); #ifdef USE_ENTITY_ICON buffer.encode_string(5, this->icon); #endif @@ -815,9 +815,9 @@ bool SwitchCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) { #endif #ifdef USE_TEXT_SENSOR void ListEntitiesTextSensorResponse::encode(ProtoWriteBuffer &buffer) const { - buffer.encode_raw_short_string(10, this->object_id); + buffer.encode_short_string(10, this->object_id); buffer.write_tag_and_fixed32(21, this->key); - buffer.encode_raw_short_string(26, this->name); + buffer.encode_short_string(26, this->name); #ifdef USE_ENTITY_ICON buffer.encode_string(5, this->icon); #endif @@ -1268,9 +1268,9 @@ uint32_t ExecuteServiceResponse::calculate_size() const { #endif #ifdef USE_CAMERA void ListEntitiesCameraResponse::encode(ProtoWriteBuffer &buffer) const { - buffer.encode_raw_short_string(10, this->object_id); + buffer.encode_short_string(10, this->object_id); buffer.write_tag_and_fixed32(21, this->key); - buffer.encode_raw_short_string(26, this->name); + buffer.encode_short_string(26, this->name); buffer.encode_bool(5, this->disabled_by_default); #ifdef USE_ENTITY_ICON buffer.encode_string(6, this->icon); @@ -1329,9 +1329,9 @@ bool CameraImageRequest::decode_varint(uint32_t field_id, proto_varint_value_t v #endif #ifdef USE_CLIMATE void ListEntitiesClimateResponse::encode(ProtoWriteBuffer &buffer) const { - buffer.encode_raw_short_string(10, this->object_id); + buffer.encode_short_string(10, this->object_id); buffer.write_tag_and_fixed32(21, this->key); - buffer.encode_raw_short_string(26, this->name); + buffer.encode_short_string(26, this->name); buffer.encode_bool(5, this->supports_current_temperature); buffer.encode_bool(6, this->supports_two_point_target_temperature); for (const auto &it : *this->supported_modes) { @@ -1562,9 +1562,9 @@ bool ClimateCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) { #endif #ifdef USE_WATER_HEATER void ListEntitiesWaterHeaterResponse::encode(ProtoWriteBuffer &buffer) const { - buffer.encode_raw_short_string(10, this->object_id); + buffer.encode_short_string(10, this->object_id); buffer.write_tag_and_fixed32(21, this->key); - buffer.encode_raw_short_string(26, this->name); + buffer.encode_short_string(26, this->name); #ifdef USE_ENTITY_ICON buffer.encode_string(4, this->icon); #endif @@ -1674,9 +1674,9 @@ bool WaterHeaterCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value #endif #ifdef USE_NUMBER void ListEntitiesNumberResponse::encode(ProtoWriteBuffer &buffer) const { - buffer.encode_raw_short_string(10, this->object_id); + buffer.encode_short_string(10, this->object_id); buffer.write_tag_and_fixed32(21, this->key); - buffer.encode_raw_short_string(26, this->name); + buffer.encode_short_string(26, this->name); #ifdef USE_ENTITY_ICON buffer.encode_string(5, this->icon); #endif @@ -1759,9 +1759,9 @@ bool NumberCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) { #endif #ifdef USE_SELECT void ListEntitiesSelectResponse::encode(ProtoWriteBuffer &buffer) const { - buffer.encode_raw_short_string(10, this->object_id); + buffer.encode_short_string(10, this->object_id); buffer.write_tag_and_fixed32(21, this->key); - buffer.encode_raw_short_string(26, this->name); + buffer.encode_short_string(26, this->name); #ifdef USE_ENTITY_ICON buffer.encode_string(5, this->icon); #endif @@ -1848,9 +1848,9 @@ bool SelectCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) { #endif #ifdef USE_SIREN void ListEntitiesSirenResponse::encode(ProtoWriteBuffer &buffer) const { - buffer.encode_raw_short_string(10, this->object_id); + buffer.encode_short_string(10, this->object_id); buffer.write_tag_and_fixed32(21, this->key); - buffer.encode_raw_short_string(26, this->name); + buffer.encode_short_string(26, this->name); #ifdef USE_ENTITY_ICON buffer.encode_string(5, this->icon); #endif @@ -1960,9 +1960,9 @@ bool SirenCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) { #endif #ifdef USE_LOCK void ListEntitiesLockResponse::encode(ProtoWriteBuffer &buffer) const { - buffer.encode_raw_short_string(10, this->object_id); + buffer.encode_short_string(10, this->object_id); buffer.write_tag_and_fixed32(21, this->key); - buffer.encode_raw_short_string(26, this->name); + buffer.encode_short_string(26, this->name); #ifdef USE_ENTITY_ICON buffer.encode_string(5, this->icon); #endif @@ -2053,9 +2053,9 @@ bool LockCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) { #endif #ifdef USE_BUTTON void ListEntitiesButtonResponse::encode(ProtoWriteBuffer &buffer) const { - buffer.encode_raw_short_string(10, this->object_id); + buffer.encode_short_string(10, this->object_id); buffer.write_tag_and_fixed32(21, this->key); - buffer.encode_raw_short_string(26, this->name); + buffer.encode_short_string(26, this->name); #ifdef USE_ENTITY_ICON buffer.encode_string(5, this->icon); #endif @@ -2123,9 +2123,9 @@ uint32_t MediaPlayerSupportedFormat::calculate_size() const { return size; } void ListEntitiesMediaPlayerResponse::encode(ProtoWriteBuffer &buffer) const { - buffer.encode_raw_short_string(10, this->object_id); + buffer.encode_short_string(10, this->object_id); buffer.write_tag_and_fixed32(21, this->key); - buffer.encode_raw_short_string(26, this->name); + buffer.encode_short_string(26, this->name); #ifdef USE_ENTITY_ICON buffer.encode_string(5, this->icon); #endif @@ -2945,9 +2945,9 @@ bool VoiceAssistantSetConfiguration::decode_length(uint32_t field_id, ProtoLengt #endif #ifdef USE_ALARM_CONTROL_PANEL void ListEntitiesAlarmControlPanelResponse::encode(ProtoWriteBuffer &buffer) const { - buffer.encode_raw_short_string(10, this->object_id); + buffer.encode_short_string(10, this->object_id); buffer.write_tag_and_fixed32(21, this->key); - buffer.encode_raw_short_string(26, this->name); + buffer.encode_short_string(26, this->name); #ifdef USE_ENTITY_ICON buffer.encode_string(5, this->icon); #endif @@ -3033,9 +3033,9 @@ bool AlarmControlPanelCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit #endif #ifdef USE_TEXT void ListEntitiesTextResponse::encode(ProtoWriteBuffer &buffer) const { - buffer.encode_raw_short_string(10, this->object_id); + buffer.encode_short_string(10, this->object_id); buffer.write_tag_and_fixed32(21, this->key); - buffer.encode_raw_short_string(26, this->name); + buffer.encode_short_string(26, this->name); #ifdef USE_ENTITY_ICON buffer.encode_string(5, this->icon); #endif @@ -3122,9 +3122,9 @@ bool TextCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) { #endif #ifdef USE_DATETIME_DATE void ListEntitiesDateResponse::encode(ProtoWriteBuffer &buffer) const { - buffer.encode_raw_short_string(10, this->object_id); + buffer.encode_short_string(10, this->object_id); buffer.write_tag_and_fixed32(21, this->key); - buffer.encode_raw_short_string(26, this->name); + buffer.encode_short_string(26, this->name); #ifdef USE_ENTITY_ICON buffer.encode_string(5, this->icon); #endif @@ -3205,9 +3205,9 @@ bool DateCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) { #endif #ifdef USE_DATETIME_TIME void ListEntitiesTimeResponse::encode(ProtoWriteBuffer &buffer) const { - buffer.encode_raw_short_string(10, this->object_id); + buffer.encode_short_string(10, this->object_id); buffer.write_tag_and_fixed32(21, this->key); - buffer.encode_raw_short_string(26, this->name); + buffer.encode_short_string(26, this->name); #ifdef USE_ENTITY_ICON buffer.encode_string(5, this->icon); #endif @@ -3288,9 +3288,9 @@ bool TimeCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) { #endif #ifdef USE_EVENT void ListEntitiesEventResponse::encode(ProtoWriteBuffer &buffer) const { - buffer.encode_raw_short_string(10, this->object_id); + buffer.encode_short_string(10, this->object_id); buffer.write_tag_and_fixed32(21, this->key); - buffer.encode_raw_short_string(26, this->name); + buffer.encode_short_string(26, this->name); #ifdef USE_ENTITY_ICON buffer.encode_string(5, this->icon); #endif @@ -3344,9 +3344,9 @@ uint32_t EventResponse::calculate_size() const { #endif #ifdef USE_VALVE void ListEntitiesValveResponse::encode(ProtoWriteBuffer &buffer) const { - buffer.encode_raw_short_string(10, this->object_id); + buffer.encode_short_string(10, this->object_id); buffer.write_tag_and_fixed32(21, this->key); - buffer.encode_raw_short_string(26, this->name); + buffer.encode_short_string(26, this->name); #ifdef USE_ENTITY_ICON buffer.encode_string(5, this->icon); #endif @@ -3431,9 +3431,9 @@ bool ValveCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) { #endif #ifdef USE_DATETIME_DATETIME void ListEntitiesDateTimeResponse::encode(ProtoWriteBuffer &buffer) const { - buffer.encode_raw_short_string(10, this->object_id); + buffer.encode_short_string(10, this->object_id); buffer.write_tag_and_fixed32(21, this->key); - buffer.encode_raw_short_string(26, this->name); + buffer.encode_short_string(26, this->name); #ifdef USE_ENTITY_ICON buffer.encode_string(5, this->icon); #endif @@ -3504,9 +3504,9 @@ bool DateTimeCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) { #endif #ifdef USE_UPDATE void ListEntitiesUpdateResponse::encode(ProtoWriteBuffer &buffer) const { - buffer.encode_raw_short_string(10, this->object_id); + buffer.encode_short_string(10, this->object_id); buffer.write_tag_and_fixed32(21, this->key); - buffer.encode_raw_short_string(26, this->name); + buffer.encode_short_string(26, this->name); #ifdef USE_ENTITY_ICON buffer.encode_string(5, this->icon); #endif @@ -3645,9 +3645,9 @@ uint32_t ZWaveProxyRequest::calculate_size() const { #endif #ifdef USE_INFRARED void ListEntitiesInfraredResponse::encode(ProtoWriteBuffer &buffer) const { - buffer.encode_raw_short_string(10, this->object_id); + buffer.encode_short_string(10, this->object_id); buffer.write_tag_and_fixed32(21, this->key); - buffer.encode_raw_short_string(26, this->name); + buffer.encode_short_string(26, this->name); #ifdef USE_ENTITY_ICON buffer.encode_string(4, this->icon); #endif diff --git a/esphome/components/api/proto.h b/esphome/components/api/proto.h index 86f0085d32..6bcd801682 100644 --- a/esphome/components/api/proto.h +++ b/esphome/components/api/proto.h @@ -242,7 +242,7 @@ class ProtoWriteBuffer { } /// Write tag + 1-byte length + raw string data. For strings with max_data_length < 128. /// Tag must be a single-byte varint (< 128). Always encodes (no zero check). - inline void encode_raw_short_string(uint8_t tag, const StringRef &ref) ESPHOME_ALWAYS_INLINE { + inline void encode_short_string(uint8_t tag, const StringRef &ref) ESPHOME_ALWAYS_INLINE { this->debug_check_bounds_(2 + ref.size()); uint8_t *__restrict__ pos = this->pos_; *pos++ = tag; diff --git a/script/api_protobuf/api_protobuf.py b/script/api_protobuf/api_protobuf.py index 644ff85b96..589e5dc140 100755 --- a/script/api_protobuf/api_protobuf.py +++ b/script/api_protobuf/api_protobuf.py @@ -1078,9 +1078,7 @@ class PointerToStringBufferType(PointerToBufferTypeBase): if max_len is not None and max_len < 128 and self.force: tag = self.calculate_tag() if tag < 128: - return ( - f"buffer.encode_raw_short_string({tag}, this->{self.field_name});" - ) + return f"buffer.encode_short_string({tag}, this->{self.field_name});" if result := self._encode_bytes_with_precomputed_tag( f"this->{self.field_name}.c_str()", f"this->{self.field_name}.size()", From 4ce24389b761ba8fa2a398578be65ee27e6601fd Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 3 Apr 2026 14:06:00 -1000 Subject: [PATCH 22/31] [api] Add max_data_length = 47 for device_class fields --- esphome/components/api/api.proto | 27 ++++++++++++++------------- esphome/components/api/api_pb2.cpp | 20 ++++++++++---------- 2 files changed, 24 insertions(+), 23 deletions(-) diff --git a/esphome/components/api/api.proto b/esphome/components/api/api.proto index 278e977a73..56423ca231 100644 --- a/esphome/components/api/api.proto +++ b/esphome/components/api/api.proto @@ -308,9 +308,10 @@ enum EntityCategory { ENTITY_CATEGORY_DIAGNOSTIC = 2; } -// Entity name/object_id max_data_length = 120 matches NAME_MAX_LENGTH, -// icon max_data_length = 63 matches ICON_MAX_LENGTH. -// Both defined in esphome/core/config.py and validated at config time. +// Entity field max_data_length values match constants in esphome/core/config.py: +// name/object_id = 120 (NAME_MAX_LENGTH) +// icon = 63 (ICON_MAX_LENGTH) +// device_class = 47 (DEVICE_CLASS_MAX_LENGTH) // ==================== BINARY SENSOR ==================== message ListEntitiesBinarySensorResponse { @@ -324,7 +325,7 @@ message ListEntitiesBinarySensorResponse { string name = 3 [(max_data_length) = 120, (force) = true]; reserved 4; // Deprecated: was string unique_id - string device_class = 5; + string device_class = 5 [(max_data_length) = 47]; bool is_status_binary_sensor = 6; bool disabled_by_default = 7; string icon = 8 [(field_ifdef) = "USE_ENTITY_ICON", (max_data_length) = 63]; @@ -361,7 +362,7 @@ message ListEntitiesCoverResponse { bool assumed_state = 5; bool supports_position = 6; bool supports_tilt = 7; - string device_class = 8; + string device_class = 8 [(max_data_length) = 47]; bool disabled_by_default = 9; string icon = 10 [(field_ifdef) = "USE_ENTITY_ICON", (max_data_length) = 63]; EntityCategory entity_category = 11; @@ -639,7 +640,7 @@ message ListEntitiesSensorResponse { string unit_of_measurement = 6; int32 accuracy_decimals = 7; bool force_update = 8; - string device_class = 9; + string device_class = 9 [(max_data_length) = 47]; SensorStateClass state_class = 10; // Last reset type removed in 2021.9.0 // Deprecated in API version 1.5 @@ -679,7 +680,7 @@ message ListEntitiesSwitchResponse { bool assumed_state = 6; bool disabled_by_default = 7; EntityCategory entity_category = 8; - string device_class = 9; + string device_class = 9 [(max_data_length) = 47]; uint32 device_id = 10 [(field_ifdef) = "USE_DEVICES"]; } message SwitchStateResponse { @@ -720,7 +721,7 @@ message ListEntitiesTextSensorResponse { string icon = 5 [(field_ifdef) = "USE_ENTITY_ICON", (max_data_length) = 63]; bool disabled_by_default = 6; EntityCategory entity_category = 7; - string device_class = 8; + string device_class = 8 [(max_data_length) = 47]; uint32 device_id = 9 [(field_ifdef) = "USE_DEVICES"]; } message TextSensorStateResponse { @@ -1260,7 +1261,7 @@ message ListEntitiesNumberResponse { EntityCategory entity_category = 10; string unit_of_measurement = 11; NumberMode mode = 12; - string device_class = 13; + string device_class = 13 [(max_data_length) = 47]; uint32 device_id = 14 [(field_ifdef) = "USE_DEVICES"]; } message NumberStateResponse { @@ -1460,7 +1461,7 @@ message ListEntitiesButtonResponse { string icon = 5 [(field_ifdef) = "USE_ENTITY_ICON", (max_data_length) = 63]; bool disabled_by_default = 6; EntityCategory entity_category = 7; - string device_class = 8; + string device_class = 8 [(max_data_length) = 47]; uint32 device_id = 9 [(field_ifdef) = "USE_DEVICES"]; } message ButtonCommandRequest { @@ -2304,7 +2305,7 @@ message ListEntitiesEventResponse { string icon = 5 [(field_ifdef) = "USE_ENTITY_ICON", (max_data_length) = 63]; bool disabled_by_default = 6; EntityCategory entity_category = 7; - string device_class = 8; + string device_class = 8 [(max_data_length) = 47]; repeated string event_types = 9 [(container_pointer_no_template) = "FixedVector"]; uint32 device_id = 10 [(field_ifdef) = "USE_DEVICES"]; @@ -2335,7 +2336,7 @@ message ListEntitiesValveResponse { string icon = 5 [(field_ifdef) = "USE_ENTITY_ICON", (max_data_length) = 63]; bool disabled_by_default = 6; EntityCategory entity_category = 7; - string device_class = 8; + string device_class = 8 [(max_data_length) = 47]; bool assumed_state = 9; bool supports_position = 10; @@ -2433,7 +2434,7 @@ message ListEntitiesUpdateResponse { string icon = 5 [(field_ifdef) = "USE_ENTITY_ICON", (max_data_length) = 63]; bool disabled_by_default = 6; EntityCategory entity_category = 7; - string device_class = 8; + string device_class = 8 [(max_data_length) = 47]; uint32 device_id = 9 [(field_ifdef) = "USE_DEVICES"]; } message UpdateStateResponse { diff --git a/esphome/components/api/api_pb2.cpp b/esphome/components/api/api_pb2.cpp index 8dea47bb6b..d00a61b692 100644 --- a/esphome/components/api/api_pb2.cpp +++ b/esphome/components/api/api_pb2.cpp @@ -226,7 +226,7 @@ uint32_t ListEntitiesBinarySensorResponse::calculate_size() const { size += 2 + this->object_id.size(); size += 5; size += 2 + this->name.size(); - size += ProtoSize::calc_length(1, this->device_class.size()); + size += !this->device_class.empty() ? 2 + this->device_class.size() : 0; size += ProtoSize::calc_bool(1, this->is_status_binary_sensor); size += ProtoSize::calc_bool(1, this->disabled_by_default); #ifdef USE_ENTITY_ICON @@ -284,7 +284,7 @@ uint32_t ListEntitiesCoverResponse::calculate_size() const { size += ProtoSize::calc_bool(1, this->assumed_state); size += ProtoSize::calc_bool(1, this->supports_position); size += ProtoSize::calc_bool(1, this->supports_tilt); - size += ProtoSize::calc_length(1, this->device_class.size()); + size += !this->device_class.empty() ? 2 + this->device_class.size() : 0; size += ProtoSize::calc_bool(1, this->disabled_by_default); #ifdef USE_ENTITY_ICON size += !this->icon.empty() ? 2 + this->icon.size() : 0; @@ -710,7 +710,7 @@ uint32_t ListEntitiesSensorResponse::calculate_size() const { size += ProtoSize::calc_length(1, this->unit_of_measurement.size()); size += ProtoSize::calc_int32(1, this->accuracy_decimals); size += ProtoSize::calc_bool(1, this->force_update); - size += ProtoSize::calc_length(1, this->device_class.size()); + size += !this->device_class.empty() ? 2 + this->device_class.size() : 0; size += ProtoSize::calc_uint32(1, static_cast(this->state_class)); size += ProtoSize::calc_bool(1, this->disabled_by_default); size += ProtoSize::calc_uint32(1, static_cast(this->entity_category)); @@ -765,7 +765,7 @@ uint32_t ListEntitiesSwitchResponse::calculate_size() const { size += ProtoSize::calc_bool(1, this->assumed_state); size += ProtoSize::calc_bool(1, this->disabled_by_default); size += ProtoSize::calc_uint32(1, static_cast(this->entity_category)); - size += ProtoSize::calc_length(1, this->device_class.size()); + size += !this->device_class.empty() ? 2 + this->device_class.size() : 0; #ifdef USE_DEVICES size += ProtoSize::calc_uint32(1, this->device_id); #endif @@ -838,7 +838,7 @@ uint32_t ListEntitiesTextSensorResponse::calculate_size() const { #endif size += ProtoSize::calc_bool(1, this->disabled_by_default); size += ProtoSize::calc_uint32(1, static_cast(this->entity_category)); - size += ProtoSize::calc_length(1, this->device_class.size()); + size += !this->device_class.empty() ? 2 + this->device_class.size() : 0; #ifdef USE_DEVICES size += ProtoSize::calc_uint32(1, this->device_id); #endif @@ -1707,7 +1707,7 @@ uint32_t ListEntitiesNumberResponse::calculate_size() const { size += ProtoSize::calc_uint32(1, static_cast(this->entity_category)); size += ProtoSize::calc_length(1, this->unit_of_measurement.size()); size += ProtoSize::calc_uint32(1, static_cast(this->mode)); - size += ProtoSize::calc_length(1, this->device_class.size()); + size += !this->device_class.empty() ? 2 + this->device_class.size() : 0; #ifdef USE_DEVICES size += ProtoSize::calc_uint32(1, this->device_id); #endif @@ -2076,7 +2076,7 @@ uint32_t ListEntitiesButtonResponse::calculate_size() const { #endif size += ProtoSize::calc_bool(1, this->disabled_by_default); size += ProtoSize::calc_uint32(1, static_cast(this->entity_category)); - size += ProtoSize::calc_length(1, this->device_class.size()); + size += !this->device_class.empty() ? 2 + this->device_class.size() : 0; #ifdef USE_DEVICES size += ProtoSize::calc_uint32(1, this->device_id); #endif @@ -3314,7 +3314,7 @@ uint32_t ListEntitiesEventResponse::calculate_size() const { #endif size += ProtoSize::calc_bool(1, this->disabled_by_default); size += ProtoSize::calc_uint32(1, static_cast(this->entity_category)); - size += ProtoSize::calc_length(1, this->device_class.size()); + size += !this->device_class.empty() ? 2 + this->device_class.size() : 0; if (!this->event_types->empty()) { for (const char *it : *this->event_types) { size += ProtoSize::calc_length_force(1, strlen(it)); @@ -3370,7 +3370,7 @@ uint32_t ListEntitiesValveResponse::calculate_size() const { #endif size += ProtoSize::calc_bool(1, this->disabled_by_default); size += ProtoSize::calc_uint32(1, static_cast(this->entity_category)); - size += ProtoSize::calc_length(1, this->device_class.size()); + size += !this->device_class.empty() ? 2 + this->device_class.size() : 0; size += ProtoSize::calc_bool(1, this->assumed_state); size += ProtoSize::calc_bool(1, this->supports_position); size += ProtoSize::calc_bool(1, this->supports_stop); @@ -3527,7 +3527,7 @@ uint32_t ListEntitiesUpdateResponse::calculate_size() const { #endif size += ProtoSize::calc_bool(1, this->disabled_by_default); size += ProtoSize::calc_uint32(1, static_cast(this->entity_category)); - size += ProtoSize::calc_length(1, this->device_class.size()); + size += !this->device_class.empty() ? 2 + this->device_class.size() : 0; #ifdef USE_DEVICES size += ProtoSize::calc_uint32(1, this->device_id); #endif From f0622b76249991f3e4e22e6a0ceae5cf3c9331cc Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 3 Apr 2026 14:10:25 -1000 Subject: [PATCH 23/31] [api] Add max_data_length = 63 for unit_of_measurement fields Add UNIT_OF_MEASUREMENT_MAX_LENGTH = 63 constant in core/config.py and enforce it in sensor and number component validators. Annotate unit_of_measurement proto fields with (max_data_length) = 63 so the codegen emits constant-size length varint calculations. --- esphome/components/api/api.proto | 3 ++- esphome/components/api/api_pb2.cpp | 2 +- esphome/components/number/__init__.py | 7 ++++++- esphome/components/sensor/__init__.py | 7 ++++++- esphome/core/config.py | 3 +++ 5 files changed, 18 insertions(+), 4 deletions(-) diff --git a/esphome/components/api/api.proto b/esphome/components/api/api.proto index 56423ca231..f67595f3b5 100644 --- a/esphome/components/api/api.proto +++ b/esphome/components/api/api.proto @@ -312,6 +312,7 @@ enum EntityCategory { // name/object_id = 120 (NAME_MAX_LENGTH) // icon = 63 (ICON_MAX_LENGTH) // device_class = 47 (DEVICE_CLASS_MAX_LENGTH) +// unit_of_measurement = 63 (UNIT_OF_MEASUREMENT_MAX_LENGTH) // ==================== BINARY SENSOR ==================== message ListEntitiesBinarySensorResponse { @@ -637,7 +638,7 @@ message ListEntitiesSensorResponse { reserved 4; // Deprecated: was string unique_id string icon = 5 [(field_ifdef) = "USE_ENTITY_ICON", (max_data_length) = 63]; - string unit_of_measurement = 6; + string unit_of_measurement = 6 [(max_data_length) = 63]; int32 accuracy_decimals = 7; bool force_update = 8; string device_class = 9 [(max_data_length) = 47]; diff --git a/esphome/components/api/api_pb2.cpp b/esphome/components/api/api_pb2.cpp index d00a61b692..e4d35f62a6 100644 --- a/esphome/components/api/api_pb2.cpp +++ b/esphome/components/api/api_pb2.cpp @@ -707,7 +707,7 @@ uint32_t ListEntitiesSensorResponse::calculate_size() const { #ifdef USE_ENTITY_ICON size += !this->icon.empty() ? 2 + this->icon.size() : 0; #endif - size += ProtoSize::calc_length(1, this->unit_of_measurement.size()); + size += !this->unit_of_measurement.empty() ? 2 + this->unit_of_measurement.size() : 0; size += ProtoSize::calc_int32(1, this->accuracy_decimals); size += ProtoSize::calc_bool(1, this->force_update); size += !this->device_class.empty() ? 2 + this->device_class.size() : 0; diff --git a/esphome/components/number/__init__.py b/esphome/components/number/__init__.py index 90f9fe1835..26d2602ba4 100644 --- a/esphome/components/number/__init__.py +++ b/esphome/components/number/__init__.py @@ -79,6 +79,7 @@ from esphome.const import ( DEVICE_CLASS_WIND_SPEED, ) from esphome.core import CORE, CoroPriority, coroutine_with_priority +from esphome.core.config import UNIT_OF_MEASUREMENT_MAX_LENGTH from esphome.core.entity_helpers import ( entity_duplicate_validator, setup_device_class, @@ -186,7 +187,11 @@ NUMBER_OPERATION_OPTIONS = { } validate_device_class = cv.one_of(*DEVICE_CLASSES, lower=True, space="_") -validate_unit_of_measurement = cv.string_strict +validate_unit_of_measurement = cv.All( + cv.string_strict, + # Keep in sync with max_data_length in api.proto + cv.Length(max=UNIT_OF_MEASUREMENT_MAX_LENGTH), +) _NUMBER_SCHEMA = ( cv.ENTITY_BASE_SCHEMA.extend(web_server.WEBSERVER_SORTING_SCHEMA) diff --git a/esphome/components/sensor/__init__.py b/esphome/components/sensor/__init__.py index 626466eefa..275c4542fb 100644 --- a/esphome/components/sensor/__init__.py +++ b/esphome/components/sensor/__init__.py @@ -106,6 +106,7 @@ from esphome.const import ( ENTITY_CATEGORY_CONFIG, ) from esphome.core import CORE, CoroPriority, coroutine_with_priority +from esphome.core.config import UNIT_OF_MEASUREMENT_MAX_LENGTH from esphome.core.entity_helpers import ( entity_duplicate_validator, setup_device_class, @@ -290,7 +291,11 @@ ClampFilter = sensor_ns.class_("ClampFilter", Filter) RoundFilter = sensor_ns.class_("RoundFilter", Filter) RoundMultipleFilter = sensor_ns.class_("RoundMultipleFilter", Filter) -validate_unit_of_measurement = cv.string_strict +validate_unit_of_measurement = cv.All( + cv.string_strict, + # Keep in sync with max_data_length in api.proto + cv.Length(max=UNIT_OF_MEASUREMENT_MAX_LENGTH), +) validate_accuracy_decimals = cv.int_ validate_icon = cv.icon validate_device_class = cv.one_of(*DEVICE_CLASSES, lower=True, space="_") diff --git a/esphome/core/config.py b/esphome/core/config.py index c47693c783..675b9296e5 100644 --- a/esphome/core/config.py +++ b/esphome/core/config.py @@ -233,6 +233,9 @@ DEVICE_CLASS_MAX_LENGTH = 47 # Keep in sync with MAX_ICON_LENGTH in esphome/core/entity_base.h ICON_MAX_LENGTH = 63 +# Max unit of measurement string length +UNIT_OF_MEASUREMENT_MAX_LENGTH = 63 + AREA_SCHEMA = cv.Schema( { cv.GenerateID(CONF_ID): cv.declare_id(Area), From 5740d74d97acd85cfdbb0ac47d0ee933bd9c4e05 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 3 Apr 2026 14:11:51 -1000 Subject: [PATCH 24/31] [core] Enforce UNIT_OF_MEASUREMENT_MAX_LENGTH in register_unit_of_measurement --- esphome/core/entity_helpers.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/esphome/core/entity_helpers.py b/esphome/core/entity_helpers.py index 0589b92364..fc931c2baa 100644 --- a/esphome/core/entity_helpers.py +++ b/esphome/core/entity_helpers.py @@ -17,7 +17,11 @@ from esphome.const import ( CONF_UNIT_OF_MEASUREMENT, ) from esphome.core import CORE, ID, CoroPriority, coroutine_with_priority -from esphome.core.config import DEVICE_CLASS_MAX_LENGTH, ICON_MAX_LENGTH +from esphome.core.config import ( + DEVICE_CLASS_MAX_LENGTH, + ICON_MAX_LENGTH, + UNIT_OF_MEASUREMENT_MAX_LENGTH, +) from esphome.cpp_generator import MockObj, RawStatement, add, get_variable import esphome.final_validate as fv from esphome.helpers import cpp_string_escape, fnv1_hash_object_id, sanitize, snake_case @@ -200,6 +204,11 @@ def register_device_class(value: str) -> int: def register_unit_of_measurement(value: str) -> int: """Register a unit_of_measurement string and return its 1-based index.""" + if value and len(value) > UNIT_OF_MEASUREMENT_MAX_LENGTH: + raise ValueError( + f"Unit of measurement string too long ({len(value)} chars, " + f"max {UNIT_OF_MEASUREMENT_MAX_LENGTH}): '{value}'" + ) return _register_string(value, _get_pool().units, _MAX_UNITS, "unit_of_measurement") From dd08d7631e990b34569033909d7f891cbb2ae5bd Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 3 Apr 2026 14:13:03 -1000 Subject: [PATCH 25/31] [core] Add test for register_unit_of_measurement max length --- tests/unit_tests/core/test_entity_helpers.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/tests/unit_tests/core/test_entity_helpers.py b/tests/unit_tests/core/test_entity_helpers.py index d6cbb8c6be..e79ff850f9 100644 --- a/tests/unit_tests/core/test_entity_helpers.py +++ b/tests/unit_tests/core/test_entity_helpers.py @@ -28,6 +28,7 @@ from esphome.core.entity_helpers import ( get_base_entity_object_id, register_device_class, register_icon, + register_unit_of_measurement, setup_device_class, setup_entity, setup_unit_of_measurement, @@ -925,6 +926,22 @@ def test_register_device_class_max_length() -> None: assert register_device_class("") == 0 +def test_register_unit_of_measurement_max_length() -> None: + """Test register_unit_of_measurement rejects units exceeding 63 characters.""" + # 63 chars should succeed + max_uom = "a" * 63 + idx = register_unit_of_measurement(max_uom) + assert idx > 0 + + # 64 chars should fail + too_long = "a" * 64 + with pytest.raises(ValueError, match="Unit of measurement string too long"): + register_unit_of_measurement(too_long) + + # Empty string returns 0 + assert register_unit_of_measurement("") == 0 + + @pytest.mark.asyncio async def test_setup_entity_with_entity_category( setup_test_environment: list[str], From 80502e8c3959a9ad727a00feeb0f846ca9727cbd Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 3 Apr 2026 14:15:19 -1000 Subject: [PATCH 26/31] [api] Rename encode_short_string to write_short_string --- esphome/components/api/api_pb2.cpp | 100 ++++++++++++++-------------- esphome/components/api/proto.h | 2 +- script/api_protobuf/api_protobuf.py | 2 +- 3 files changed, 52 insertions(+), 52 deletions(-) diff --git a/esphome/components/api/api_pb2.cpp b/esphome/components/api/api_pb2.cpp index e4d35f62a6..8c51122a4c 100644 --- a/esphome/components/api/api_pb2.cpp +++ b/esphome/components/api/api_pb2.cpp @@ -207,9 +207,9 @@ uint32_t DeviceInfoResponse::calculate_size() const { } #ifdef USE_BINARY_SENSOR void ListEntitiesBinarySensorResponse::encode(ProtoWriteBuffer &buffer) const { - buffer.encode_short_string(10, this->object_id); + buffer.write_short_string(10, this->object_id); buffer.write_tag_and_fixed32(21, this->key); - buffer.encode_short_string(26, this->name); + buffer.write_short_string(26, this->name); buffer.encode_string(5, this->device_class); buffer.encode_bool(6, this->is_status_binary_sensor); buffer.encode_bool(7, this->disabled_by_default); @@ -259,9 +259,9 @@ uint32_t BinarySensorStateResponse::calculate_size() const { #endif #ifdef USE_COVER void ListEntitiesCoverResponse::encode(ProtoWriteBuffer &buffer) const { - buffer.encode_short_string(10, this->object_id); + buffer.write_short_string(10, this->object_id); buffer.write_tag_and_fixed32(21, this->key); - buffer.encode_short_string(26, this->name); + buffer.write_short_string(26, this->name); buffer.encode_bool(5, this->assumed_state); buffer.encode_bool(6, this->supports_position); buffer.encode_bool(7, this->supports_tilt); @@ -356,9 +356,9 @@ bool CoverCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) { #endif #ifdef USE_FAN void ListEntitiesFanResponse::encode(ProtoWriteBuffer &buffer) const { - buffer.encode_short_string(10, this->object_id); + buffer.write_short_string(10, this->object_id); buffer.write_tag_and_fixed32(21, this->key); - buffer.encode_short_string(26, this->name); + buffer.write_short_string(26, this->name); buffer.encode_bool(5, this->supports_oscillation); buffer.encode_bool(6, this->supports_speed); buffer.encode_bool(7, this->supports_direction); @@ -486,9 +486,9 @@ bool FanCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) { #endif #ifdef USE_LIGHT void ListEntitiesLightResponse::encode(ProtoWriteBuffer &buffer) const { - buffer.encode_short_string(10, this->object_id); + buffer.write_short_string(10, this->object_id); buffer.write_tag_and_fixed32(21, this->key); - buffer.encode_short_string(26, this->name); + buffer.write_short_string(26, this->name); for (const auto &it : *this->supported_color_modes) { buffer.encode_uint32(12, static_cast(it), true); } @@ -682,9 +682,9 @@ bool LightCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) { #endif #ifdef USE_SENSOR void ListEntitiesSensorResponse::encode(ProtoWriteBuffer &buffer) const { - buffer.encode_short_string(10, this->object_id); + buffer.write_short_string(10, this->object_id); buffer.write_tag_and_fixed32(21, this->key); - buffer.encode_short_string(26, this->name); + buffer.write_short_string(26, this->name); #ifdef USE_ENTITY_ICON buffer.encode_string(5, this->icon); #endif @@ -740,9 +740,9 @@ uint32_t SensorStateResponse::calculate_size() const { #endif #ifdef USE_SWITCH void ListEntitiesSwitchResponse::encode(ProtoWriteBuffer &buffer) const { - buffer.encode_short_string(10, this->object_id); + buffer.write_short_string(10, this->object_id); buffer.write_tag_and_fixed32(21, this->key); - buffer.encode_short_string(26, this->name); + buffer.write_short_string(26, this->name); #ifdef USE_ENTITY_ICON buffer.encode_string(5, this->icon); #endif @@ -815,9 +815,9 @@ bool SwitchCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) { #endif #ifdef USE_TEXT_SENSOR void ListEntitiesTextSensorResponse::encode(ProtoWriteBuffer &buffer) const { - buffer.encode_short_string(10, this->object_id); + buffer.write_short_string(10, this->object_id); buffer.write_tag_and_fixed32(21, this->key); - buffer.encode_short_string(26, this->name); + buffer.write_short_string(26, this->name); #ifdef USE_ENTITY_ICON buffer.encode_string(5, this->icon); #endif @@ -1268,9 +1268,9 @@ uint32_t ExecuteServiceResponse::calculate_size() const { #endif #ifdef USE_CAMERA void ListEntitiesCameraResponse::encode(ProtoWriteBuffer &buffer) const { - buffer.encode_short_string(10, this->object_id); + buffer.write_short_string(10, this->object_id); buffer.write_tag_and_fixed32(21, this->key); - buffer.encode_short_string(26, this->name); + buffer.write_short_string(26, this->name); buffer.encode_bool(5, this->disabled_by_default); #ifdef USE_ENTITY_ICON buffer.encode_string(6, this->icon); @@ -1329,9 +1329,9 @@ bool CameraImageRequest::decode_varint(uint32_t field_id, proto_varint_value_t v #endif #ifdef USE_CLIMATE void ListEntitiesClimateResponse::encode(ProtoWriteBuffer &buffer) const { - buffer.encode_short_string(10, this->object_id); + buffer.write_short_string(10, this->object_id); buffer.write_tag_and_fixed32(21, this->key); - buffer.encode_short_string(26, this->name); + buffer.write_short_string(26, this->name); buffer.encode_bool(5, this->supports_current_temperature); buffer.encode_bool(6, this->supports_two_point_target_temperature); for (const auto &it : *this->supported_modes) { @@ -1562,9 +1562,9 @@ bool ClimateCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) { #endif #ifdef USE_WATER_HEATER void ListEntitiesWaterHeaterResponse::encode(ProtoWriteBuffer &buffer) const { - buffer.encode_short_string(10, this->object_id); + buffer.write_short_string(10, this->object_id); buffer.write_tag_and_fixed32(21, this->key); - buffer.encode_short_string(26, this->name); + buffer.write_short_string(26, this->name); #ifdef USE_ENTITY_ICON buffer.encode_string(4, this->icon); #endif @@ -1674,9 +1674,9 @@ bool WaterHeaterCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value #endif #ifdef USE_NUMBER void ListEntitiesNumberResponse::encode(ProtoWriteBuffer &buffer) const { - buffer.encode_short_string(10, this->object_id); + buffer.write_short_string(10, this->object_id); buffer.write_tag_and_fixed32(21, this->key); - buffer.encode_short_string(26, this->name); + buffer.write_short_string(26, this->name); #ifdef USE_ENTITY_ICON buffer.encode_string(5, this->icon); #endif @@ -1759,9 +1759,9 @@ bool NumberCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) { #endif #ifdef USE_SELECT void ListEntitiesSelectResponse::encode(ProtoWriteBuffer &buffer) const { - buffer.encode_short_string(10, this->object_id); + buffer.write_short_string(10, this->object_id); buffer.write_tag_and_fixed32(21, this->key); - buffer.encode_short_string(26, this->name); + buffer.write_short_string(26, this->name); #ifdef USE_ENTITY_ICON buffer.encode_string(5, this->icon); #endif @@ -1848,9 +1848,9 @@ bool SelectCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) { #endif #ifdef USE_SIREN void ListEntitiesSirenResponse::encode(ProtoWriteBuffer &buffer) const { - buffer.encode_short_string(10, this->object_id); + buffer.write_short_string(10, this->object_id); buffer.write_tag_and_fixed32(21, this->key); - buffer.encode_short_string(26, this->name); + buffer.write_short_string(26, this->name); #ifdef USE_ENTITY_ICON buffer.encode_string(5, this->icon); #endif @@ -1960,9 +1960,9 @@ bool SirenCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) { #endif #ifdef USE_LOCK void ListEntitiesLockResponse::encode(ProtoWriteBuffer &buffer) const { - buffer.encode_short_string(10, this->object_id); + buffer.write_short_string(10, this->object_id); buffer.write_tag_and_fixed32(21, this->key); - buffer.encode_short_string(26, this->name); + buffer.write_short_string(26, this->name); #ifdef USE_ENTITY_ICON buffer.encode_string(5, this->icon); #endif @@ -2053,9 +2053,9 @@ bool LockCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) { #endif #ifdef USE_BUTTON void ListEntitiesButtonResponse::encode(ProtoWriteBuffer &buffer) const { - buffer.encode_short_string(10, this->object_id); + buffer.write_short_string(10, this->object_id); buffer.write_tag_and_fixed32(21, this->key); - buffer.encode_short_string(26, this->name); + buffer.write_short_string(26, this->name); #ifdef USE_ENTITY_ICON buffer.encode_string(5, this->icon); #endif @@ -2123,9 +2123,9 @@ uint32_t MediaPlayerSupportedFormat::calculate_size() const { return size; } void ListEntitiesMediaPlayerResponse::encode(ProtoWriteBuffer &buffer) const { - buffer.encode_short_string(10, this->object_id); + buffer.write_short_string(10, this->object_id); buffer.write_tag_and_fixed32(21, this->key); - buffer.encode_short_string(26, this->name); + buffer.write_short_string(26, this->name); #ifdef USE_ENTITY_ICON buffer.encode_string(5, this->icon); #endif @@ -2945,9 +2945,9 @@ bool VoiceAssistantSetConfiguration::decode_length(uint32_t field_id, ProtoLengt #endif #ifdef USE_ALARM_CONTROL_PANEL void ListEntitiesAlarmControlPanelResponse::encode(ProtoWriteBuffer &buffer) const { - buffer.encode_short_string(10, this->object_id); + buffer.write_short_string(10, this->object_id); buffer.write_tag_and_fixed32(21, this->key); - buffer.encode_short_string(26, this->name); + buffer.write_short_string(26, this->name); #ifdef USE_ENTITY_ICON buffer.encode_string(5, this->icon); #endif @@ -3033,9 +3033,9 @@ bool AlarmControlPanelCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit #endif #ifdef USE_TEXT void ListEntitiesTextResponse::encode(ProtoWriteBuffer &buffer) const { - buffer.encode_short_string(10, this->object_id); + buffer.write_short_string(10, this->object_id); buffer.write_tag_and_fixed32(21, this->key); - buffer.encode_short_string(26, this->name); + buffer.write_short_string(26, this->name); #ifdef USE_ENTITY_ICON buffer.encode_string(5, this->icon); #endif @@ -3122,9 +3122,9 @@ bool TextCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) { #endif #ifdef USE_DATETIME_DATE void ListEntitiesDateResponse::encode(ProtoWriteBuffer &buffer) const { - buffer.encode_short_string(10, this->object_id); + buffer.write_short_string(10, this->object_id); buffer.write_tag_and_fixed32(21, this->key); - buffer.encode_short_string(26, this->name); + buffer.write_short_string(26, this->name); #ifdef USE_ENTITY_ICON buffer.encode_string(5, this->icon); #endif @@ -3205,9 +3205,9 @@ bool DateCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) { #endif #ifdef USE_DATETIME_TIME void ListEntitiesTimeResponse::encode(ProtoWriteBuffer &buffer) const { - buffer.encode_short_string(10, this->object_id); + buffer.write_short_string(10, this->object_id); buffer.write_tag_and_fixed32(21, this->key); - buffer.encode_short_string(26, this->name); + buffer.write_short_string(26, this->name); #ifdef USE_ENTITY_ICON buffer.encode_string(5, this->icon); #endif @@ -3288,9 +3288,9 @@ bool TimeCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) { #endif #ifdef USE_EVENT void ListEntitiesEventResponse::encode(ProtoWriteBuffer &buffer) const { - buffer.encode_short_string(10, this->object_id); + buffer.write_short_string(10, this->object_id); buffer.write_tag_and_fixed32(21, this->key); - buffer.encode_short_string(26, this->name); + buffer.write_short_string(26, this->name); #ifdef USE_ENTITY_ICON buffer.encode_string(5, this->icon); #endif @@ -3344,9 +3344,9 @@ uint32_t EventResponse::calculate_size() const { #endif #ifdef USE_VALVE void ListEntitiesValveResponse::encode(ProtoWriteBuffer &buffer) const { - buffer.encode_short_string(10, this->object_id); + buffer.write_short_string(10, this->object_id); buffer.write_tag_and_fixed32(21, this->key); - buffer.encode_short_string(26, this->name); + buffer.write_short_string(26, this->name); #ifdef USE_ENTITY_ICON buffer.encode_string(5, this->icon); #endif @@ -3431,9 +3431,9 @@ bool ValveCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) { #endif #ifdef USE_DATETIME_DATETIME void ListEntitiesDateTimeResponse::encode(ProtoWriteBuffer &buffer) const { - buffer.encode_short_string(10, this->object_id); + buffer.write_short_string(10, this->object_id); buffer.write_tag_and_fixed32(21, this->key); - buffer.encode_short_string(26, this->name); + buffer.write_short_string(26, this->name); #ifdef USE_ENTITY_ICON buffer.encode_string(5, this->icon); #endif @@ -3504,9 +3504,9 @@ bool DateTimeCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) { #endif #ifdef USE_UPDATE void ListEntitiesUpdateResponse::encode(ProtoWriteBuffer &buffer) const { - buffer.encode_short_string(10, this->object_id); + buffer.write_short_string(10, this->object_id); buffer.write_tag_and_fixed32(21, this->key); - buffer.encode_short_string(26, this->name); + buffer.write_short_string(26, this->name); #ifdef USE_ENTITY_ICON buffer.encode_string(5, this->icon); #endif @@ -3645,9 +3645,9 @@ uint32_t ZWaveProxyRequest::calculate_size() const { #endif #ifdef USE_INFRARED void ListEntitiesInfraredResponse::encode(ProtoWriteBuffer &buffer) const { - buffer.encode_short_string(10, this->object_id); + buffer.write_short_string(10, this->object_id); buffer.write_tag_and_fixed32(21, this->key); - buffer.encode_short_string(26, this->name); + buffer.write_short_string(26, this->name); #ifdef USE_ENTITY_ICON buffer.encode_string(4, this->icon); #endif diff --git a/esphome/components/api/proto.h b/esphome/components/api/proto.h index 6bcd801682..33c8f80d1e 100644 --- a/esphome/components/api/proto.h +++ b/esphome/components/api/proto.h @@ -242,7 +242,7 @@ class ProtoWriteBuffer { } /// Write tag + 1-byte length + raw string data. For strings with max_data_length < 128. /// Tag must be a single-byte varint (< 128). Always encodes (no zero check). - inline void encode_short_string(uint8_t tag, const StringRef &ref) ESPHOME_ALWAYS_INLINE { + inline void write_short_string(uint8_t tag, const StringRef &ref) ESPHOME_ALWAYS_INLINE { this->debug_check_bounds_(2 + ref.size()); uint8_t *__restrict__ pos = this->pos_; *pos++ = tag; diff --git a/script/api_protobuf/api_protobuf.py b/script/api_protobuf/api_protobuf.py index 589e5dc140..6b8d63184c 100755 --- a/script/api_protobuf/api_protobuf.py +++ b/script/api_protobuf/api_protobuf.py @@ -1078,7 +1078,7 @@ class PointerToStringBufferType(PointerToBufferTypeBase): if max_len is not None and max_len < 128 and self.force: tag = self.calculate_tag() if tag < 128: - return f"buffer.encode_short_string({tag}, this->{self.field_name});" + return f"buffer.write_short_string({tag}, this->{self.field_name});" if result := self._encode_bytes_with_precomputed_tag( f"this->{self.field_name}.c_str()", f"this->{self.field_name}.size()", From 308877cf6ea42614ed954d9dd237b165f0ce3cf1 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 3 Apr 2026 14:32:42 -1000 Subject: [PATCH 27/31] [web_server] Disable loop when no SSE clients are connected --- esphome/components/web_server/web_server.cpp | 15 +++++++++++++-- esphome/components/web_server/web_server.h | 3 ++- .../components/web_server_idf/web_server_idf.cpp | 6 +++++- .../components/web_server_idf/web_server_idf.h | 3 ++- 4 files changed, 22 insertions(+), 5 deletions(-) diff --git a/esphome/components/web_server/web_server.cpp b/esphome/components/web_server/web_server.cpp index 1dda6204fe..c2d8844b5a 100644 --- a/esphome/components/web_server/web_server.cpp +++ b/esphome/components/web_server/web_server.cpp @@ -286,10 +286,11 @@ void DeferredUpdateEventSource::try_send_nodefer(const char *message, const char this->send(message, event, id, reconnect); } -void DeferredUpdateEventSourceList::loop() { +bool DeferredUpdateEventSourceList::loop() { for (DeferredUpdateEventSource *dues : *this) { dues->loop(); } + return !this->empty(); } void DeferredUpdateEventSourceList::deferrable_send_state(void *source, const char *event_type, @@ -318,6 +319,7 @@ void DeferredUpdateEventSourceList::add_new_client(WebServer *ws, AsyncWebServer es->onDisconnect([this, es](AsyncEventSourceClient *client) { this->on_client_disconnect_(es); }); es->handleRequest(request); + ws->enable_loop_soon_any_context(); } void DeferredUpdateEventSourceList::on_client_connect_(DeferredUpdateEventSource *source) { @@ -419,7 +421,16 @@ void WebServer::setup() { this->events_.try_send_nodefer(buf, "ping", millis(), 30000); }); } -void WebServer::loop() { this->events_.loop(); } +void WebServer::loop() { + // No SSE clients connected; stop looping until a new client connects via + // enable_loop_soon_any_context(). This is safe because: + // - set_interval/set_timeout/defer run via the Scheduler, independent of loop() + // - deferrable_send_state early-outs when no clients are connected + // - try_send_nodefer (log, ping) iterates sessions which are empty + // - REST API handlers use defer() which runs via the Scheduler + if (!this->events_.loop()) + this->disable_loop(); +} #ifdef USE_LOGGER void WebServer::on_log(uint8_t level, const char *tag, const char *message, size_t message_len) { diff --git a/esphome/components/web_server/web_server.h b/esphome/components/web_server/web_server.h index 6152dfbfd3..51a9c9d9d6 100644 --- a/esphome/components/web_server/web_server.h +++ b/esphome/components/web_server/web_server.h @@ -169,7 +169,8 @@ class DeferredUpdateEventSourceList final : public std::liston_connect_(rsp); } this->sessions_.push_back(rsp); + // Wake up WebServer::loop() to drain deferred event queues for this client. + // Safe from httpd task context via the pending_enable_loop_ flag. + this->web_server_->enable_loop_soon_any_context(); } -void AsyncEventSource::loop() { +bool AsyncEventSource::loop() { // Clean up dead sessions safely // This follows the ESP-IDF pattern where free_ctx marks resources as dead // and the main loop handles the actual cleanup to avoid race conditions @@ -504,6 +507,7 @@ void AsyncEventSource::loop() { ++i; } } + return !this->sessions_.empty(); } void AsyncEventSource::try_send_nodefer(const char *message, const char *event, uint32_t id, uint32_t reconnect) { diff --git a/esphome/components/web_server_idf/web_server_idf.h b/esphome/components/web_server_idf/web_server_idf.h index 81683e8d85..7137e294fa 100644 --- a/esphome/components/web_server_idf/web_server_idf.h +++ b/esphome/components/web_server_idf/web_server_idf.h @@ -340,7 +340,8 @@ class AsyncEventSource : public AsyncWebHandler { void try_send_nodefer(const char *message, const char *event = nullptr, uint32_t id = 0, uint32_t reconnect = 0); void deferrable_send_state(void *source, const char *event_type, message_generator_t *message_generator); - void loop(); + /// Returns true if there are still connected clients. + bool loop(); bool empty() { return this->count() == 0; } size_t count() const { return this->sessions_.size(); } From fec8fe932fb899e9a25bbf175bbe813a6cfd7a3a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 3 Apr 2026 14:49:02 -1000 Subject: [PATCH 28/31] [web_server] Skip ping interval when no SSE clients are connected --- esphome/components/web_server/web_server.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/esphome/components/web_server/web_server.cpp b/esphome/components/web_server/web_server.cpp index c2d8844b5a..a57a8d26ff 100644 --- a/esphome/components/web_server/web_server.cpp +++ b/esphome/components/web_server/web_server.cpp @@ -415,6 +415,8 @@ void WebServer::setup() { // doesn't need defer functionality - if the queue is full, the client JS knows it's alive because it's clearly // getting a lot of events this->set_interval(10000, [this]() { + if (this->events_.empty()) + return; char buf[32]; auto uptime = static_cast(millis_64() / 1000); buf_append_printf(buf, sizeof(buf), 0, "{\"uptime\":%" PRIu32 "}", uptime); From 5cec03b00424c7a2dbfd34780cc785de4b9e300f Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 3 Apr 2026 17:03:03 -1000 Subject: [PATCH 29/31] [hlw8012] Change periodic sensor reading logs to LOGV --- esphome/components/hlw8012/hlw8012.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/esphome/components/hlw8012/hlw8012.cpp b/esphome/components/hlw8012/hlw8012.cpp index d0fd697d8f..22f292e47e 100644 --- a/esphome/components/hlw8012/hlw8012.cpp +++ b/esphome/components/hlw8012/hlw8012.cpp @@ -73,13 +73,13 @@ void HLW8012Component::update() { // Only read cf1 after one cycle. Apparently it's quite unstable after being changed. if (this->current_mode_) { float current = cf1_hz * this->current_multiplier_; - ESP_LOGD(TAG, "Got power=%.1fW, current=%.1fA", power, current); + ESP_LOGV(TAG, "Got power=%.1fW, current=%.1fA", power, current); if (this->current_sensor_ != nullptr) { this->current_sensor_->publish_state(current); } } else { float voltage = cf1_hz * this->voltage_multiplier_; - ESP_LOGD(TAG, "Got power=%.1fW, voltage=%.1fV", power, voltage); + ESP_LOGV(TAG, "Got power=%.1fW, voltage=%.1fV", power, voltage); if (this->voltage_sensor_ != nullptr) { this->voltage_sensor_->publish_state(voltage); } From 089b38f77aedb4cbce608779b41d93c7a6498a30 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 3 Apr 2026 17:17:57 -1000 Subject: [PATCH 30/31] [total_daily_energy] Replace loop() with timeout-based midnight reset --- .../total_daily_energy/total_daily_energy.cpp | 56 +++++++++++++------ .../total_daily_energy/total_daily_energy.h | 8 +-- 2 files changed, 43 insertions(+), 21 deletions(-) diff --git a/esphome/components/total_daily_energy/total_daily_energy.cpp b/esphome/components/total_daily_energy/total_daily_energy.cpp index e7a45a5edf..2c823be55c 100644 --- a/esphome/components/total_daily_energy/total_daily_energy.cpp +++ b/esphome/components/total_daily_energy/total_daily_energy.cpp @@ -1,10 +1,16 @@ #include "total_daily_energy.h" +#include "esphome/core/application.h" #include "esphome/core/log.h" -namespace esphome { -namespace total_daily_energy { +namespace esphome::total_daily_energy { static const char *const TAG = "total_daily_energy"; +static constexpr uint32_t MIDNIGHT_TIMEOUT = 1; +static constexpr uint8_t SECONDS_PER_MINUTE = 60; +static constexpr uint8_t MINUTES_PER_HOUR = 60; +static constexpr uint8_t HOURS_PER_DAY = 24; +static constexpr uint32_t SECONDS_PER_HOUR = SECONDS_PER_MINUTE * MINUTES_PER_HOUR; +static constexpr uint16_t MS_PER_SECOND = 1000; void TotalDailyEnergy::setup() { float initial_value = 0; @@ -15,28 +21,47 @@ void TotalDailyEnergy::setup() { } this->publish_state_and_save(initial_value); - this->last_update_ = millis(); + this->last_update_ = App.get_loop_component_start_time(); this->parent_->add_on_state_callback([this](float state) { this->process_new_state_(state); }); + + // Schedule initial midnight reset if time is already valid, otherwise + // the time sync callback will handle it once time becomes available. + this->schedule_midnight_reset_(); + // Re-schedule on every NTP sync in case the clock jumped across midnight. + // DST transitions don't trigger this callback (DST is a local time interpretation, + // not an epoch change), but DST is handled correctly because set_timeout uses real + // elapsed time (millis) and schedule_midnight_reset_ re-reads now() when it fires. + this->time_->add_on_time_sync_callback([this]() { this->schedule_midnight_reset_(); }); } void TotalDailyEnergy::dump_config() { LOG_SENSOR("", "Total Daily Energy", this); } -void TotalDailyEnergy::loop() { +void TotalDailyEnergy::schedule_midnight_reset_() { auto t = this->time_->now(); if (!t.is_valid()) return; - if (this->last_day_of_year_ == 0) { + // Check if the day changed (time sync moved us past midnight, or first call) + if (this->last_day_of_year_ != t.day_of_year) { + if (this->last_day_of_year_ != 0) { + // Day actually changed — reset energy + this->total_energy_ = 0; + this->publish_state_and_save(0); + } this->last_day_of_year_ = t.day_of_year; - return; } - if (t.day_of_year != this->last_day_of_year_) { - this->last_day_of_year_ = t.day_of_year; - this->total_energy_ = 0; - this->publish_state_and_save(0); - } + // Calculate seconds until next midnight (+ 1s buffer to ensure we're past midnight). + // Uses the same MIDNIGHT_TIMEOUT ID so re-scheduling (e.g. from time sync) cancels + // any previously pending timeout. + uint32_t seconds_until_midnight = + ((HOURS_PER_DAY - 1 - t.hour) * MINUTES_PER_HOUR + (MINUTES_PER_HOUR - 1 - t.minute)) * SECONDS_PER_MINUTE + + (SECONDS_PER_MINUTE - t.second) + 1; + + ESP_LOGD(TAG, "Scheduling midnight reset in %us", seconds_until_midnight); + this->set_timeout(MIDNIGHT_TIMEOUT, seconds_until_midnight * MS_PER_SECOND, + [this]() { this->schedule_midnight_reset_(); }); } void TotalDailyEnergy::publish_state_and_save(float state) { @@ -50,14 +75,14 @@ void TotalDailyEnergy::publish_state_and_save(float state) { void TotalDailyEnergy::process_new_state_(float state) { if (std::isnan(state)) return; - const uint32_t now = millis(); + const uint32_t now = App.get_loop_component_start_time(); const float old_state = this->last_power_state_; const float new_state = state; - float delta_hours = (now - this->last_update_) / 1000.0f / 60.0f / 60.0f; + float delta_hours = (now - this->last_update_) / static_cast(MS_PER_SECOND) / SECONDS_PER_HOUR; float delta_energy = 0.0f; switch (this->method_) { case TOTAL_DAILY_ENERGY_METHOD_TRAPEZOID: - delta_energy = delta_hours * (old_state + new_state) / 2.0; + delta_energy = delta_hours * (old_state + new_state) / 2.0f; break; case TOTAL_DAILY_ENERGY_METHOD_LEFT: delta_energy = delta_hours * old_state; @@ -71,5 +96,4 @@ void TotalDailyEnergy::process_new_state_(float state) { this->publish_state_and_save(this->total_energy_ + delta_energy); } -} // namespace total_daily_energy -} // namespace esphome +} // namespace esphome::total_daily_energy diff --git a/esphome/components/total_daily_energy/total_daily_energy.h b/esphome/components/total_daily_energy/total_daily_energy.h index 1145f54f95..9a20ecea01 100644 --- a/esphome/components/total_daily_energy/total_daily_energy.h +++ b/esphome/components/total_daily_energy/total_daily_energy.h @@ -6,8 +6,7 @@ #include "esphome/components/sensor/sensor.h" #include "esphome/components/time/real_time_clock.h" -namespace esphome { -namespace total_daily_energy { +namespace esphome::total_daily_energy { enum TotalDailyEnergyMethod { TOTAL_DAILY_ENERGY_METHOD_TRAPEZOID = 0, @@ -23,12 +22,12 @@ class TotalDailyEnergy : public sensor::Sensor, public Component { void set_method(TotalDailyEnergyMethod method) { method_ = method; } void setup() override; void dump_config() override; - void loop() override; void publish_state_and_save(float state); protected: void process_new_state_(float state); + void schedule_midnight_reset_(); ESPPreferenceObject pref_; time::RealTimeClock *time_; @@ -41,5 +40,4 @@ class TotalDailyEnergy : public sensor::Sensor, public Component { float last_power_state_{0.0f}; }; -} // namespace total_daily_energy -} // namespace esphome +} // namespace esphome::total_daily_energy From 5658a2054c3f74036b97c478d9e5c2e15e8a7339 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 3 Apr 2026 17:17:57 -1000 Subject: [PATCH 31/31] [total_daily_energy] Replace loop() with timeout-based midnight reset --- .../total_daily_energy/total_daily_energy.cpp | 65 ++++++++++++++----- .../total_daily_energy/total_daily_energy.h | 8 +-- 2 files changed, 53 insertions(+), 20 deletions(-) diff --git a/esphome/components/total_daily_energy/total_daily_energy.cpp b/esphome/components/total_daily_energy/total_daily_energy.cpp index e7a45a5edf..b79df784f6 100644 --- a/esphome/components/total_daily_energy/total_daily_energy.cpp +++ b/esphome/components/total_daily_energy/total_daily_energy.cpp @@ -1,10 +1,21 @@ #include "total_daily_energy.h" +#include "esphome/core/application.h" #include "esphome/core/log.h" -namespace esphome { -namespace total_daily_energy { +namespace esphome::total_daily_energy { static const char *const TAG = "total_daily_energy"; +static constexpr uint32_t TIMEOUT_ID_MIDNIGHT = 1; +static constexpr uint8_t SECONDS_PER_MINUTE = 60; +static constexpr uint8_t MINUTES_PER_HOUR = 60; +static constexpr uint8_t HOURS_PER_DAY = 24; +static constexpr uint32_t SECONDS_PER_HOUR = SECONDS_PER_MINUTE * MINUTES_PER_HOUR; +static constexpr uint16_t MILLIS_PER_SECOND = 1000; +// Wake up 90 minutes before midnight to recalculate, ensuring DST transitions +// (which shift wall clock by 1 hour but don't change millis()) don't cause +// the midnight reset to fire late. DST transitions don't trigger the time sync +// callback since they change local time interpretation, not the epoch. +static constexpr uint32_t PRE_MIDNIGHT_SECONDS = 90 * SECONDS_PER_MINUTE; void TotalDailyEnergy::setup() { float initial_value = 0; @@ -15,28 +26,53 @@ void TotalDailyEnergy::setup() { } this->publish_state_and_save(initial_value); - this->last_update_ = millis(); + this->last_update_ = App.get_loop_component_start_time(); this->parent_->add_on_state_callback([this](float state) { this->process_new_state_(state); }); + + // Schedule initial midnight reset if time is already valid, otherwise + // the time sync callback will handle it once time becomes available. + this->schedule_midnight_reset_(); + // Re-schedule on every NTP sync in case the clock jumped across midnight. + this->time_->add_on_time_sync_callback([this]() { this->schedule_midnight_reset_(); }); } void TotalDailyEnergy::dump_config() { LOG_SENSOR("", "Total Daily Energy", this); } -void TotalDailyEnergy::loop() { +void TotalDailyEnergy::schedule_midnight_reset_() { auto t = this->time_->now(); if (!t.is_valid()) return; - if (this->last_day_of_year_ == 0) { + // Check if the day changed (time sync moved us past midnight, or first call) + if (this->last_day_of_year_ != t.day_of_year) { + if (this->last_day_of_year_ != 0) { + // Day actually changed — reset energy + this->total_energy_ = 0; + this->publish_state_and_save(0); + } this->last_day_of_year_ = t.day_of_year; - return; } - if (t.day_of_year != this->last_day_of_year_) { - this->last_day_of_year_ = t.day_of_year; - this->total_energy_ = 0; - this->publish_state_and_save(0); + // Calculate seconds until next midnight. + // Uses the same TIMEOUT_ID_MIDNIGHT ID so re-scheduling (e.g. from time sync) cancels + // any previously pending timeout. + uint32_t seconds_until_midnight = + ((HOURS_PER_DAY - 1 - t.hour) * MINUTES_PER_HOUR + (MINUTES_PER_HOUR - 1 - t.minute)) * SECONDS_PER_MINUTE + + (SECONDS_PER_MINUTE - t.second); + + uint32_t timeout_seconds; + if (seconds_until_midnight > PRE_MIDNIGHT_SECONDS) { + // Far from midnight — wake up 90 minutes before to recalculate with fresh wall clock + timeout_seconds = seconds_until_midnight - PRE_MIDNIGHT_SECONDS; + } else { + // Close to midnight — schedule the actual reset with 1s buffer + timeout_seconds = seconds_until_midnight + 1; } + + ESP_LOGD(TAG, "Scheduling midnight check in %us", timeout_seconds); + this->set_timeout(TIMEOUT_ID_MIDNIGHT, timeout_seconds * MILLIS_PER_SECOND, + [this]() { this->schedule_midnight_reset_(); }); } void TotalDailyEnergy::publish_state_and_save(float state) { @@ -50,14 +86,14 @@ void TotalDailyEnergy::publish_state_and_save(float state) { void TotalDailyEnergy::process_new_state_(float state) { if (std::isnan(state)) return; - const uint32_t now = millis(); + const uint32_t now = App.get_loop_component_start_time(); const float old_state = this->last_power_state_; const float new_state = state; - float delta_hours = (now - this->last_update_) / 1000.0f / 60.0f / 60.0f; + float delta_hours = (now - this->last_update_) / static_cast(MILLIS_PER_SECOND) / SECONDS_PER_HOUR; float delta_energy = 0.0f; switch (this->method_) { case TOTAL_DAILY_ENERGY_METHOD_TRAPEZOID: - delta_energy = delta_hours * (old_state + new_state) / 2.0; + delta_energy = delta_hours * (old_state + new_state) / 2.0f; break; case TOTAL_DAILY_ENERGY_METHOD_LEFT: delta_energy = delta_hours * old_state; @@ -71,5 +107,4 @@ void TotalDailyEnergy::process_new_state_(float state) { this->publish_state_and_save(this->total_energy_ + delta_energy); } -} // namespace total_daily_energy -} // namespace esphome +} // namespace esphome::total_daily_energy diff --git a/esphome/components/total_daily_energy/total_daily_energy.h b/esphome/components/total_daily_energy/total_daily_energy.h index 1145f54f95..9a20ecea01 100644 --- a/esphome/components/total_daily_energy/total_daily_energy.h +++ b/esphome/components/total_daily_energy/total_daily_energy.h @@ -6,8 +6,7 @@ #include "esphome/components/sensor/sensor.h" #include "esphome/components/time/real_time_clock.h" -namespace esphome { -namespace total_daily_energy { +namespace esphome::total_daily_energy { enum TotalDailyEnergyMethod { TOTAL_DAILY_ENERGY_METHOD_TRAPEZOID = 0, @@ -23,12 +22,12 @@ class TotalDailyEnergy : public sensor::Sensor, public Component { void set_method(TotalDailyEnergyMethod method) { method_ = method; } void setup() override; void dump_config() override; - void loop() override; void publish_state_and_save(float state); protected: void process_new_state_(float state); + void schedule_midnight_reset_(); ESPPreferenceObject pref_; time::RealTimeClock *time_; @@ -41,5 +40,4 @@ class TotalDailyEnergy : public sensor::Sensor, public Component { float last_power_state_{0.0f}; }; -} // namespace total_daily_energy -} // namespace esphome +} // namespace esphome::total_daily_energy