From 856cb182fcc097aef4d697adee6b34573070fc66 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 15 Jul 2025 15:12:12 -1000 Subject: [PATCH 01/14] Remove dead code: 64-bit protobuf types never used in 7 years (#9471) --- esphome/components/api/proto.h | 67 ++++------------------------- script/api_protobuf/api_protobuf.py | 42 ++++++++++++++---- 2 files changed, 42 insertions(+), 67 deletions(-) diff --git a/esphome/components/api/proto.h b/esphome/components/api/proto.h index a435168821..f8539f4be1 100644 --- a/esphome/components/api/proto.h +++ b/esphome/components/api/proto.h @@ -175,23 +175,7 @@ class Proto32Bit { const uint32_t value_; }; -class Proto64Bit { - public: - explicit Proto64Bit(uint64_t value) : value_(value) {} - uint64_t as_fixed64() const { return this->value_; } - int64_t as_sfixed64() const { return static_cast(this->value_); } - double as_double() const { - union { - uint64_t raw; - double value; - } s{}; - s.raw = this->value_; - return s.value; - } - - protected: - const uint64_t value_; -}; +// NOTE: Proto64Bit class removed - wire type 1 (64-bit fixed) not supported class ProtoWriteBuffer { public: @@ -258,20 +242,10 @@ class ProtoWriteBuffer { this->write((value >> 16) & 0xFF); this->write((value >> 24) & 0xFF); } - void encode_fixed64(uint32_t field_id, uint64_t value, bool force = false) { - if (value == 0 && !force) - return; - - this->encode_field_raw(field_id, 1); // type 1: 64-bit fixed64 - this->write((value >> 0) & 0xFF); - this->write((value >> 8) & 0xFF); - this->write((value >> 16) & 0xFF); - this->write((value >> 24) & 0xFF); - this->write((value >> 32) & 0xFF); - this->write((value >> 40) & 0xFF); - this->write((value >> 48) & 0xFF); - this->write((value >> 56) & 0xFF); - } + // NOTE: Wire type 1 (64-bit fixed: double, fixed64, sfixed64) is intentionally + // not supported to reduce overhead on embedded systems. All ESPHome devices are + // 32-bit microcontrollers where 64-bit operations are expensive. If 64-bit support + // is needed in the future, the necessary encoding/decoding functions must be added. void encode_float(uint32_t field_id, float value, bool force = false) { if (value == 0.0f && !force) return; @@ -337,7 +311,7 @@ class ProtoMessage { virtual bool decode_varint(uint32_t field_id, ProtoVarInt value) { return false; } virtual bool decode_length(uint32_t field_id, ProtoLengthDelimited value) { return false; } virtual bool decode_32bit(uint32_t field_id, Proto32Bit value) { return false; } - virtual bool decode_64bit(uint32_t field_id, Proto64Bit value) { return false; } + // NOTE: decode_64bit removed - wire type 1 not supported }; class ProtoSize { @@ -662,33 +636,8 @@ class ProtoSize { total_size += field_id_size + varint(value); } - /** - * @brief Calculates and adds the size of a sint64 field to the total message size - * - * Sint64 fields use ZigZag encoding, which is more efficient for negative values. - */ - static inline void add_sint64_field(uint32_t &total_size, uint32_t field_id_size, int64_t value) { - // Skip calculation if value is zero - if (value == 0) { - return; // No need to update total_size - } - - // ZigZag encoding for sint64: (n << 1) ^ (n >> 63) - uint64_t zigzag = (static_cast(value) << 1) ^ (static_cast(value >> 63)); - total_size += field_id_size + varint(zigzag); - } - - /** - * @brief Calculates and adds the size of a sint64 field to the total message size (repeated field version) - * - * Sint64 fields use ZigZag encoding, which is more efficient for negative values. - */ - static inline void add_sint64_field_repeated(uint32_t &total_size, uint32_t field_id_size, int64_t value) { - // Always calculate size for repeated fields - // ZigZag encoding for sint64: (n << 1) ^ (n >> 63) - uint64_t zigzag = (static_cast(value) << 1) ^ (static_cast(value >> 63)); - total_size += field_id_size + varint(zigzag); - } + // NOTE: sint64 support functions (add_sint64_field, add_sint64_field_repeated) removed + // sint64 type is not supported by ESPHome API to reduce overhead on embedded systems /** * @brief Calculates and adds the size of a string/bytes field to the total message size diff --git a/script/api_protobuf/api_protobuf.py b/script/api_protobuf/api_protobuf.py index 3ae1b195e4..01135bd63d 100755 --- a/script/api_protobuf/api_protobuf.py +++ b/script/api_protobuf/api_protobuf.py @@ -313,6 +313,37 @@ class TypeInfo(ABC): TYPE_INFO: dict[int, TypeInfo] = {} +# Unsupported 64-bit types that would add overhead for embedded systems +# TYPE_DOUBLE = 1, TYPE_FIXED64 = 6, TYPE_SFIXED64 = 16, TYPE_SINT64 = 18 +UNSUPPORTED_TYPES = {1: "double", 6: "fixed64", 16: "sfixed64", 18: "sint64"} + + +def validate_field_type(field_type: int, field_name: str = "") -> None: + """Validate that the field type is supported by ESPHome API. + + Raises ValueError for unsupported 64-bit types. + """ + if field_type in UNSUPPORTED_TYPES: + type_name = UNSUPPORTED_TYPES[field_type] + field_info = f" (field: {field_name})" if field_name else "" + raise ValueError( + f"64-bit type '{type_name}'{field_info} is not supported by ESPHome API. " + "These types add significant overhead for embedded systems. " + "If you need 64-bit support, please add the necessary encoding/decoding " + "functions to proto.h/proto.cpp first." + ) + + +def get_type_info_for_field(field: descriptor.FieldDescriptorProto) -> TypeInfo: + """Get the appropriate TypeInfo for a field, handling repeated fields. + + Also validates that the field type is supported. + """ + if field.label == 3: # repeated + return RepeatedTypeInfo(field) + validate_field_type(field.type, field.name) + return TYPE_INFO[field.type](field) + def register_type(name: int): """Decorator to register a type with a name and number.""" @@ -738,6 +769,7 @@ class SInt64Type(TypeInfo): class RepeatedTypeInfo(TypeInfo): def __init__(self, field: descriptor.FieldDescriptorProto) -> None: super().__init__(field) + validate_field_type(field.type, field.name) self._ti: TypeInfo = TYPE_INFO[field.type](field) @property @@ -1025,10 +1057,7 @@ def calculate_message_estimated_size(desc: descriptor.DescriptorProto) -> int: total_size = 0 for field in desc.field: - if field.label == 3: # repeated - ti = RepeatedTypeInfo(field) - else: - ti = TYPE_INFO[field.type](field) + ti = get_type_info_for_field(field) # Add estimated size for this field total_size += ti.get_estimated_size() @@ -1334,10 +1363,7 @@ def build_base_class( # For base classes, we only declare the fields but don't handle encode/decode # The derived classes will handle encoding/decoding with their specific field numbers for field in common_fields: - if field.label == 3: # repeated - ti = RepeatedTypeInfo(field) - else: - ti = TYPE_INFO[field.type](field) + ti = get_type_info_for_field(field) # Only add field declarations, not encode/decode logic protected_content.extend(ti.protected_content) From e012fd5b32bcf97951af90a7ec6e880f52c157f2 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 15 Jul 2025 15:13:51 -1000 Subject: [PATCH 02/14] Add runtime_stats component for performance debugging and analysis (#9386) Co-authored-by: Keith Burzinski --- CODEOWNERS | 1 + esphome/components/runtime_stats/__init__.py | 34 +++++ .../runtime_stats/runtime_stats.cpp | 102 ++++++++++++++ .../components/runtime_stats/runtime_stats.h | 132 ++++++++++++++++++ esphome/core/application.cpp | 11 ++ esphome/core/component.cpp | 10 ++ tests/components/runtime_stats/common.yaml | 2 + .../runtime_stats/test.esp32-ard.yaml | 1 + tests/integration/fixtures/runtime_stats.yaml | 39 ++++++ tests/integration/test_runtime_stats.py | 88 ++++++++++++ 10 files changed, 420 insertions(+) create mode 100644 esphome/components/runtime_stats/__init__.py create mode 100644 esphome/components/runtime_stats/runtime_stats.cpp create mode 100644 esphome/components/runtime_stats/runtime_stats.h create mode 100644 tests/components/runtime_stats/common.yaml create mode 100644 tests/components/runtime_stats/test.esp32-ard.yaml create mode 100644 tests/integration/fixtures/runtime_stats.yaml create mode 100644 tests/integration/test_runtime_stats.py diff --git a/CODEOWNERS b/CODEOWNERS index b5037a6f9f..257f927fd9 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -379,6 +379,7 @@ esphome/components/rp2040_pwm/* @jesserockz esphome/components/rpi_dpi_rgb/* @clydebarrow esphome/components/rtl87xx/* @kuba2k2 esphome/components/rtttl/* @glmnet +esphome/components/runtime_stats/* @bdraco esphome/components/safe_mode/* @jsuanet @kbx81 @paulmonigatti esphome/components/scd4x/* @martgras @sjtrny esphome/components/script/* @esphome/core diff --git a/esphome/components/runtime_stats/__init__.py b/esphome/components/runtime_stats/__init__.py new file mode 100644 index 0000000000..a36e8bfd28 --- /dev/null +++ b/esphome/components/runtime_stats/__init__.py @@ -0,0 +1,34 @@ +""" +Runtime statistics component for ESPHome. +""" + +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.const import CONF_ID + +CODEOWNERS = ["@bdraco"] + +CONF_LOG_INTERVAL = "log_interval" + +runtime_stats_ns = cg.esphome_ns.namespace("runtime_stats") +RuntimeStatsCollector = runtime_stats_ns.class_("RuntimeStatsCollector") + +CONFIG_SCHEMA = cv.Schema( + { + cv.GenerateID(): cv.declare_id(RuntimeStatsCollector), + cv.Optional( + CONF_LOG_INTERVAL, default="60s" + ): cv.positive_time_period_milliseconds, + } +) + + +async def to_code(config): + """Generate code for the runtime statistics component.""" + # Define USE_RUNTIME_STATS when this component is used + cg.add_define("USE_RUNTIME_STATS") + + # Create the runtime stats instance (constructor sets global_runtime_stats) + var = cg.new_Pvariable(config[CONF_ID]) + + cg.add(var.set_log_interval(config[CONF_LOG_INTERVAL])) diff --git a/esphome/components/runtime_stats/runtime_stats.cpp b/esphome/components/runtime_stats/runtime_stats.cpp new file mode 100644 index 0000000000..8f5d5daf01 --- /dev/null +++ b/esphome/components/runtime_stats/runtime_stats.cpp @@ -0,0 +1,102 @@ +#include "runtime_stats.h" + +#ifdef USE_RUNTIME_STATS + +#include "esphome/core/component.h" +#include + +namespace esphome { + +namespace runtime_stats { + +RuntimeStatsCollector::RuntimeStatsCollector() : log_interval_(60000), next_log_time_(0) { + global_runtime_stats = this; +} + +void RuntimeStatsCollector::record_component_time(Component *component, uint32_t duration_ms, uint32_t current_time) { + if (component == nullptr) + return; + + // Check if we have cached the name for this component + auto name_it = this->component_names_cache_.find(component); + if (name_it == this->component_names_cache_.end()) { + // First time seeing this component, cache its name + const char *source = component->get_component_source(); + this->component_names_cache_[component] = source; + this->component_stats_[source].record_time(duration_ms); + } else { + this->component_stats_[name_it->second].record_time(duration_ms); + } + + if (this->next_log_time_ == 0) { + this->next_log_time_ = current_time + this->log_interval_; + return; + } +} + +void RuntimeStatsCollector::log_stats_() { + ESP_LOGI(TAG, "Component Runtime Statistics"); + ESP_LOGI(TAG, "Period stats (last %" PRIu32 "ms):", this->log_interval_); + + // First collect stats we want to display + std::vector stats_to_display; + + for (const auto &it : this->component_stats_) { + const ComponentRuntimeStats &stats = it.second; + if (stats.get_period_count() > 0) { + ComponentStatPair pair = {it.first, &stats}; + stats_to_display.push_back(pair); + } + } + + // Sort by period runtime (descending) + std::sort(stats_to_display.begin(), stats_to_display.end(), std::greater()); + + // Log top components by period runtime + for (const auto &it : stats_to_display) { + const char *source = it.name; + const ComponentRuntimeStats *stats = it.stats; + + ESP_LOGI(TAG, " %s: count=%" PRIu32 ", avg=%.2fms, max=%" PRIu32 "ms, total=%" PRIu32 "ms", source, + stats->get_period_count(), stats->get_period_avg_time_ms(), stats->get_period_max_time_ms(), + stats->get_period_time_ms()); + } + + // Log total stats since boot + ESP_LOGI(TAG, "Total stats (since boot):"); + + // Re-sort by total runtime for all-time stats + std::sort(stats_to_display.begin(), stats_to_display.end(), + [](const ComponentStatPair &a, const ComponentStatPair &b) { + return a.stats->get_total_time_ms() > b.stats->get_total_time_ms(); + }); + + for (const auto &it : stats_to_display) { + const char *source = it.name; + const ComponentRuntimeStats *stats = it.stats; + + ESP_LOGI(TAG, " %s: count=%" PRIu32 ", avg=%.2fms, max=%" PRIu32 "ms, total=%" PRIu32 "ms", source, + stats->get_total_count(), stats->get_total_avg_time_ms(), stats->get_total_max_time_ms(), + stats->get_total_time_ms()); + } +} + +void RuntimeStatsCollector::process_pending_stats(uint32_t current_time) { + if (this->next_log_time_ == 0) + return; + + if (current_time >= this->next_log_time_) { + this->log_stats_(); + this->reset_stats_(); + this->next_log_time_ = current_time + this->log_interval_; + } +} + +} // namespace runtime_stats + +runtime_stats::RuntimeStatsCollector *global_runtime_stats = + nullptr; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) + +} // namespace esphome + +#endif // USE_RUNTIME_STATS diff --git a/esphome/components/runtime_stats/runtime_stats.h b/esphome/components/runtime_stats/runtime_stats.h new file mode 100644 index 0000000000..e2f8bee563 --- /dev/null +++ b/esphome/components/runtime_stats/runtime_stats.h @@ -0,0 +1,132 @@ +#pragma once + +#include "esphome/core/defines.h" + +#ifdef USE_RUNTIME_STATS + +#include +#include +#include +#include +#include "esphome/core/helpers.h" +#include "esphome/core/log.h" + +namespace esphome { + +class Component; // Forward declaration + +namespace runtime_stats { + +static const char *const TAG = "runtime_stats"; + +class ComponentRuntimeStats { + public: + ComponentRuntimeStats() + : period_count_(0), + period_time_ms_(0), + period_max_time_ms_(0), + total_count_(0), + total_time_ms_(0), + total_max_time_ms_(0) {} + + void record_time(uint32_t duration_ms) { + // Update period counters + this->period_count_++; + this->period_time_ms_ += duration_ms; + if (duration_ms > this->period_max_time_ms_) + this->period_max_time_ms_ = duration_ms; + + // Update total counters + this->total_count_++; + this->total_time_ms_ += duration_ms; + if (duration_ms > this->total_max_time_ms_) + this->total_max_time_ms_ = duration_ms; + } + + void reset_period_stats() { + this->period_count_ = 0; + this->period_time_ms_ = 0; + this->period_max_time_ms_ = 0; + } + + // Period stats (reset each logging interval) + uint32_t get_period_count() const { return this->period_count_; } + uint32_t get_period_time_ms() const { return this->period_time_ms_; } + uint32_t get_period_max_time_ms() const { return this->period_max_time_ms_; } + float get_period_avg_time_ms() const { + return this->period_count_ > 0 ? this->period_time_ms_ / static_cast(this->period_count_) : 0.0f; + } + + // Total stats (persistent until reboot) + uint32_t get_total_count() const { return this->total_count_; } + uint32_t get_total_time_ms() const { return this->total_time_ms_; } + uint32_t get_total_max_time_ms() const { return this->total_max_time_ms_; } + float get_total_avg_time_ms() const { + return this->total_count_ > 0 ? this->total_time_ms_ / static_cast(this->total_count_) : 0.0f; + } + + protected: + // Period stats (reset each logging interval) + uint32_t period_count_; + uint32_t period_time_ms_; + uint32_t period_max_time_ms_; + + // Total stats (persistent until reboot) + uint32_t total_count_; + uint32_t total_time_ms_; + uint32_t total_max_time_ms_; +}; + +// For sorting components by run time +struct ComponentStatPair { + const char *name; + const ComponentRuntimeStats *stats; + + bool operator>(const ComponentStatPair &other) const { + // Sort by period time as that's what we're displaying in the logs + return stats->get_period_time_ms() > other.stats->get_period_time_ms(); + } +}; + +class RuntimeStatsCollector { + public: + RuntimeStatsCollector(); + + void set_log_interval(uint32_t log_interval) { this->log_interval_ = log_interval; } + uint32_t get_log_interval() const { return this->log_interval_; } + + void record_component_time(Component *component, uint32_t duration_ms, uint32_t current_time); + + // Process any pending stats printing (should be called after component loop) + void process_pending_stats(uint32_t current_time); + + protected: + void log_stats_(); + + void reset_stats_() { + for (auto &it : this->component_stats_) { + it.second.reset_period_stats(); + } + } + + // Use const char* keys for efficiency + // Custom comparator for const char* keys in map + // Without this, std::map would compare pointer addresses instead of string contents, + // causing identical component names at different addresses to be treated as different keys + struct CStrCompare { + bool operator()(const char *a, const char *b) const { return std::strcmp(a, b) < 0; } + }; + std::map component_stats_; + std::map component_names_cache_; + uint32_t log_interval_; + uint32_t next_log_time_; +}; + +} // namespace runtime_stats + +extern runtime_stats::RuntimeStatsCollector + *global_runtime_stats; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) + +} // namespace esphome + +#endif // USE_RUNTIME_STATS diff --git a/esphome/core/application.cpp b/esphome/core/application.cpp index e19acd3ba6..123d6d01f4 100644 --- a/esphome/core/application.cpp +++ b/esphome/core/application.cpp @@ -4,6 +4,9 @@ #include "esphome/core/hal.h" #include #include +#ifdef USE_RUNTIME_STATS +#include "esphome/components/runtime_stats/runtime_stats.h" +#endif #ifdef USE_STATUS_LED #include "esphome/components/status_led/status_led.h" @@ -141,6 +144,14 @@ void Application::loop() { this->in_loop_ = false; this->app_state_ = new_app_state; +#ifdef USE_RUNTIME_STATS + // Process any pending runtime stats printing after all components have run + // This ensures stats printing doesn't affect component timing measurements + if (global_runtime_stats != nullptr) { + global_runtime_stats->process_pending_stats(last_op_end_time); + } +#endif + // Use the last component's end time instead of calling millis() again auto elapsed = last_op_end_time - this->last_loop_; if (elapsed >= this->loop_interval_ || HighFrequencyLoopRequester::is_high_frequency()) { diff --git a/esphome/core/component.cpp b/esphome/core/component.cpp index c47f16b5f7..623b521026 100644 --- a/esphome/core/component.cpp +++ b/esphome/core/component.cpp @@ -9,6 +9,9 @@ #include "esphome/core/hal.h" #include "esphome/core/helpers.h" #include "esphome/core/log.h" +#ifdef USE_RUNTIME_STATS +#include "esphome/components/runtime_stats/runtime_stats.h" +#endif namespace esphome { @@ -396,6 +399,13 @@ uint32_t WarnIfComponentBlockingGuard::finish() { uint32_t curr_time = millis(); uint32_t blocking_time = curr_time - this->started_; + +#ifdef USE_RUNTIME_STATS + // Record component runtime stats + if (global_runtime_stats != nullptr) { + global_runtime_stats->record_component_time(this->component_, blocking_time, curr_time); + } +#endif bool should_warn; if (this->component_ != nullptr) { should_warn = this->component_->should_warn_of_blocking(blocking_time); diff --git a/tests/components/runtime_stats/common.yaml b/tests/components/runtime_stats/common.yaml new file mode 100644 index 0000000000..b434d1b5a7 --- /dev/null +++ b/tests/components/runtime_stats/common.yaml @@ -0,0 +1,2 @@ +# Test runtime_stats component with default configuration +runtime_stats: diff --git a/tests/components/runtime_stats/test.esp32-ard.yaml b/tests/components/runtime_stats/test.esp32-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/runtime_stats/test.esp32-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/integration/fixtures/runtime_stats.yaml b/tests/integration/fixtures/runtime_stats.yaml new file mode 100644 index 0000000000..aad1c275fb --- /dev/null +++ b/tests/integration/fixtures/runtime_stats.yaml @@ -0,0 +1,39 @@ +esphome: + name: runtime-stats-test + +host: + +api: + +logger: + level: DEBUG + logs: + runtime_stats: INFO + +runtime_stats: + log_interval: 1s + +# Add some components that will execute periodically to generate stats +sensor: + - platform: template + name: "Test Sensor 1" + id: test_sensor_1 + lambda: return 42.0; + update_interval: 0.1s + + - platform: template + name: "Test Sensor 2" + id: test_sensor_2 + lambda: return 24.0; + update_interval: 0.2s + +switch: + - platform: template + name: "Test Switch" + id: test_switch + optimistic: true + +interval: + - interval: 0.5s + then: + - switch.toggle: test_switch diff --git a/tests/integration/test_runtime_stats.py b/tests/integration/test_runtime_stats.py new file mode 100644 index 0000000000..cd8546facc --- /dev/null +++ b/tests/integration/test_runtime_stats.py @@ -0,0 +1,88 @@ +"""Test runtime statistics component.""" + +from __future__ import annotations + +import asyncio +import re + +import pytest + +from .types import APIClientConnectedFactory, RunCompiledFunction + + +@pytest.mark.asyncio +async def test_runtime_stats( + yaml_config: str, + run_compiled: RunCompiledFunction, + api_client_connected: APIClientConnectedFactory, +) -> None: + """Test runtime stats logs statistics at configured interval and tracks components.""" + loop = asyncio.get_running_loop() + + # Track how many times we see the total stats + stats_count = 0 + first_stats_future = loop.create_future() + second_stats_future = loop.create_future() + + # Track component stats + component_stats_found = set() + + # Patterns to match - need to handle ANSI color codes and timestamps + # The log format is: [HH:MM:SS][color codes][I][tag]: message + total_stats_pattern = re.compile(r"Total stats \(since boot\):") + # Match component names that may include dots (e.g., template.sensor) + component_pattern = re.compile( + r"^\[[^\]]+\].*?\s+([\w.]+):\s+count=(\d+),\s+avg=([\d.]+)ms" + ) + + def check_output(line: str) -> None: + """Check log output for runtime stats messages.""" + nonlocal stats_count + + # Check for total stats line + if total_stats_pattern.search(line): + stats_count += 1 + + if stats_count == 1 and not first_stats_future.done(): + first_stats_future.set_result(True) + elif stats_count == 2 and not second_stats_future.done(): + second_stats_future.set_result(True) + + # Check for component stats + match = component_pattern.match(line) + if match: + component_name = match.group(1) + component_stats_found.add(component_name) + + async with ( + run_compiled(yaml_config, line_callback=check_output), + api_client_connected() as client, + ): + # Verify device is connected + device_info = await client.device_info() + assert device_info is not None + + # Wait for first "Total stats" log (should happen at 1s) + try: + await asyncio.wait_for(first_stats_future, timeout=5.0) + except asyncio.TimeoutError: + pytest.fail("First 'Total stats' log not seen within 5 seconds") + + # Wait for second "Total stats" log (should happen at 2s) + try: + await asyncio.wait_for(second_stats_future, timeout=5.0) + except asyncio.TimeoutError: + pytest.fail(f"Second 'Total stats' log not seen. Total seen: {stats_count}") + + # Verify we got at least 2 stats logs + assert stats_count >= 2, ( + f"Expected at least 2 'Total stats' logs, got {stats_count}" + ) + + # Verify we found stats for our components + assert "template.sensor" in component_stats_found, ( + f"Expected template.sensor stats, found: {component_stats_found}" + ) + assert "template.switch" in component_stats_found, ( + f"Expected template.switch stats, found: {component_stats_found}" + ) From 5c2dea79efe87be8eef9d8e72050eda66a66588b Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 15 Jul 2025 15:14:43 -1000 Subject: [PATCH 03/14] Make API ConnectRequest optional for passwordless connections (#9445) --- esphome/components/api/api_connection.cpp | 38 +++++++++---- esphome/components/api/api_connection.h | 3 ++ esphome/components/api/api_server.cpp | 2 - esphome/components/api/api_server.h | 1 - .../fixtures/host_mode_api_password.yaml | 14 +++++ .../test_host_mode_api_password.py | 53 +++++++++++++++++++ 6 files changed, 97 insertions(+), 14 deletions(-) create mode 100644 tests/integration/fixtures/host_mode_api_password.yaml create mode 100644 tests/integration/test_host_mode_api_password.py diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index ea3268a583..ca5d3a97ba 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -1435,6 +1435,24 @@ bool APIConnection::try_send_log_message(int level, const char *tag, const char return this->send_buffer(buffer, SubscribeLogsResponse::MESSAGE_TYPE); } +void APIConnection::complete_authentication_() { + // Early return if already authenticated + if (this->flags_.connection_state == static_cast(ConnectionState::AUTHENTICATED)) { + return; + } + + this->flags_.connection_state = static_cast(ConnectionState::AUTHENTICATED); + ESP_LOGD(TAG, "%s connected", this->get_client_combined_info().c_str()); +#ifdef USE_API_CLIENT_CONNECTED_TRIGGER + this->parent_->get_client_connected_trigger()->trigger(this->client_info_, this->client_peername_); +#endif +#ifdef USE_HOMEASSISTANT_TIME + if (homeassistant::global_homeassistant_time != nullptr) { + this->send_time_request(); + } +#endif +} + HelloResponse APIConnection::hello(const HelloRequest &msg) { this->client_info_ = msg.client_info; this->client_peername_ = this->helper_->getpeername(); @@ -1450,7 +1468,14 @@ HelloResponse APIConnection::hello(const HelloRequest &msg) { resp.server_info = App.get_name() + " (esphome v" ESPHOME_VERSION ")"; resp.name = App.get_name(); +#ifdef USE_API_PASSWORD + // Password required - wait for authentication this->flags_.connection_state = static_cast(ConnectionState::CONNECTED); +#else + // No password configured - auto-authenticate + this->complete_authentication_(); +#endif + return resp; } ConnectResponse APIConnection::connect(const ConnectRequest &msg) { @@ -1463,23 +1488,14 @@ ConnectResponse APIConnection::connect(const ConnectRequest &msg) { // bool invalid_password = 1; resp.invalid_password = !correct; if (correct) { - ESP_LOGD(TAG, "%s connected", this->get_client_combined_info().c_str()); - this->flags_.connection_state = static_cast(ConnectionState::AUTHENTICATED); -#ifdef USE_API_CLIENT_CONNECTED_TRIGGER - this->parent_->get_client_connected_trigger()->trigger(this->client_info_, this->client_peername_); -#endif -#ifdef USE_HOMEASSISTANT_TIME - if (homeassistant::global_homeassistant_time != nullptr) { - this->send_time_request(); - } -#endif + this->complete_authentication_(); } return resp; } DeviceInfoResponse APIConnection::device_info(const DeviceInfoRequest &msg) { DeviceInfoResponse resp{}; #ifdef USE_API_PASSWORD - resp.uses_password = this->parent_->uses_password(); + resp.uses_password = true; #else resp.uses_password = false; #endif diff --git a/esphome/components/api/api_connection.h b/esphome/components/api/api_connection.h index 0051a143de..0a3cb7b4d4 100644 --- a/esphome/components/api/api_connection.h +++ b/esphome/components/api/api_connection.h @@ -273,6 +273,9 @@ class APIConnection : public APIServerConnection { ProtoWriteBuffer allocate_batch_message_buffer(uint16_t size); protected: + // Helper function to handle authentication completion + void complete_authentication_(); + // Helper function to fill common entity info fields static void fill_entity_info_base(esphome::EntityBase *entity, InfoResponseProtoMessage &response) { // Set common fields that are shared by all entity types diff --git a/esphome/components/api/api_server.cpp b/esphome/components/api/api_server.cpp index f5be672c9a..5b87a773b5 100644 --- a/esphome/components/api/api_server.cpp +++ b/esphome/components/api/api_server.cpp @@ -219,8 +219,6 @@ void APIServer::dump_config() { } #ifdef USE_API_PASSWORD -bool APIServer::uses_password() const { return !this->password_.empty(); } - bool APIServer::check_password(const std::string &password) const { // depend only on input password length const char *a = this->password_.c_str(); diff --git a/esphome/components/api/api_server.h b/esphome/components/api/api_server.h index f41064b62b..edbd289421 100644 --- a/esphome/components/api/api_server.h +++ b/esphome/components/api/api_server.h @@ -39,7 +39,6 @@ class APIServer : public Component, public Controller { bool teardown() override; #ifdef USE_API_PASSWORD bool check_password(const std::string &password) const; - bool uses_password() const; void set_password(const std::string &password); #endif void set_port(uint16_t port); diff --git a/tests/integration/fixtures/host_mode_api_password.yaml b/tests/integration/fixtures/host_mode_api_password.yaml new file mode 100644 index 0000000000..038b6871e0 --- /dev/null +++ b/tests/integration/fixtures/host_mode_api_password.yaml @@ -0,0 +1,14 @@ +esphome: + name: host-mode-api-password +host: +api: + password: "test_password_123" +logger: + level: DEBUG +# Test sensor to verify connection works +sensor: + - platform: template + name: Test Sensor + id: test_sensor + lambda: return 42.0; + update_interval: 0.1s diff --git a/tests/integration/test_host_mode_api_password.py b/tests/integration/test_host_mode_api_password.py new file mode 100644 index 0000000000..098fc38142 --- /dev/null +++ b/tests/integration/test_host_mode_api_password.py @@ -0,0 +1,53 @@ +"""Integration test for API password authentication.""" + +from __future__ import annotations + +import asyncio + +from aioesphomeapi import APIConnectionError +import pytest + +from .types import APIClientConnectedFactory, RunCompiledFunction + + +@pytest.mark.asyncio +async def test_host_mode_api_password( + yaml_config: str, + run_compiled: RunCompiledFunction, + api_client_connected: APIClientConnectedFactory, +) -> None: + """Test API authentication with password.""" + async with run_compiled(yaml_config): + # Connect with correct password + async with api_client_connected(password="test_password_123") as client: + # Verify we can get device info + device_info = await client.device_info() + assert device_info is not None + assert device_info.uses_password is True + assert device_info.name == "host-mode-api-password" + + # Subscribe to states to ensure authenticated connection works + loop = asyncio.get_running_loop() + state_future: asyncio.Future[bool] = loop.create_future() + states = {} + + def on_state(state): + states[state.key] = state + if not state_future.done(): + state_future.set_result(True) + + client.subscribe_states(on_state) + + # Wait for at least one state with timeout + try: + await asyncio.wait_for(state_future, timeout=5.0) + except asyncio.TimeoutError: + pytest.fail("No states received within timeout") + + # Should have received at least one state (the test sensor) + assert len(states) > 0 + + # Test with wrong password - should fail + with pytest.raises(APIConnectionError, match="Invalid password"): + async with api_client_connected(password="wrong_password"): + pass # Should not reach here From b5be45273f6010ce4a4613cf4a9d74b8d4b6edb6 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 15 Jul 2025 15:15:11 -1000 Subject: [PATCH 04/14] Improve API protobuf decode method readability and reduce code size (#9455) --- esphome/components/api/api_pb2.cpp | 1307 ++++++++++++--------------- script/api_protobuf/api_protobuf.py | 94 +- 2 files changed, 606 insertions(+), 795 deletions(-) diff --git a/esphome/components/api/api_pb2.cpp b/esphome/components/api/api_pb2.cpp index 4c0e20e0f0..797a33bfbd 100644 --- a/esphome/components/api/api_pb2.cpp +++ b/esphome/components/api/api_pb2.cpp @@ -9,27 +9,26 @@ namespace api { bool HelloRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { - case 2: { + case 2: this->api_version_major = value.as_uint32(); - return true; - } - case 3: { + break; + case 3: this->api_version_minor = value.as_uint32(); - return true; - } + break; default: return false; } + return true; } bool HelloRequest::decode_length(uint32_t field_id, ProtoLengthDelimited value) { switch (field_id) { - case 1: { + case 1: this->client_info = value.as_string(); - return true; - } + break; default: return false; } + return true; } void HelloResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_uint32(1, this->api_version_major); @@ -45,13 +44,13 @@ void HelloResponse::calculate_size(uint32_t &total_size) const { } bool ConnectRequest::decode_length(uint32_t field_id, ProtoLengthDelimited value) { switch (field_id) { - case 1: { + case 1: this->password = value.as_string(); - return true; - } + break; default: return false; } + return true; } void ConnectResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_bool(1, this->invalid_password); } void ConnectResponse::calculate_size(uint32_t &total_size) const { @@ -59,23 +58,23 @@ void ConnectResponse::calculate_size(uint32_t &total_size) const { } bool AreaInfo::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { - case 1: { + case 1: this->area_id = value.as_uint32(); - return true; - } + break; default: return false; } + return true; } bool AreaInfo::decode_length(uint32_t field_id, ProtoLengthDelimited value) { switch (field_id) { - case 2: { + case 2: this->name = value.as_string(); - return true; - } + break; default: return false; } + return true; } void AreaInfo::encode(ProtoWriteBuffer buffer) const { buffer.encode_uint32(1, this->area_id); @@ -87,27 +86,26 @@ void AreaInfo::calculate_size(uint32_t &total_size) const { } bool DeviceInfo::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { - case 1: { + case 1: this->device_id = value.as_uint32(); - return true; - } - case 3: { + break; + case 3: this->area_id = value.as_uint32(); - return true; - } + break; default: return false; } + return true; } bool DeviceInfo::decode_length(uint32_t field_id, ProtoLengthDelimited value) { switch (field_id) { - case 2: { + case 2: this->name = value.as_string(); - return true; - } + break; default: return false; } + return true; } void DeviceInfo::encode(ProtoWriteBuffer buffer) const { buffer.encode_uint32(1, this->device_id); @@ -258,51 +256,44 @@ void CoverStateResponse::calculate_size(uint32_t &total_size) const { } bool CoverCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { - case 2: { + case 2: this->has_legacy_command = value.as_bool(); - return true; - } - case 3: { + break; + case 3: this->legacy_command = static_cast(value.as_uint32()); - return true; - } - case 4: { + break; + case 4: this->has_position = value.as_bool(); - return true; - } - case 6: { + break; + case 6: this->has_tilt = value.as_bool(); - return true; - } - case 8: { + break; + case 8: this->stop = value.as_bool(); - return true; - } - case 9: { + break; + case 9: this->device_id = value.as_uint32(); - return true; - } + break; default: return false; } + return true; } bool CoverCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) { switch (field_id) { - case 1: { + case 1: this->key = value.as_fixed32(); - return true; - } - case 5: { + break; + case 5: this->position = value.as_float(); - return true; - } - case 7: { + break; + case 7: this->tilt = value.as_float(); - return true; - } + break; default: return false; } + return true; } #endif #ifdef USE_FAN @@ -364,77 +355,66 @@ void FanStateResponse::calculate_size(uint32_t &total_size) const { } bool FanCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { - case 2: { + case 2: this->has_state = value.as_bool(); - return true; - } - case 3: { + break; + case 3: this->state = value.as_bool(); - return true; - } - case 4: { + break; + case 4: this->has_speed = value.as_bool(); - return true; - } - case 5: { + break; + case 5: this->speed = static_cast(value.as_uint32()); - return true; - } - case 6: { + break; + case 6: this->has_oscillating = value.as_bool(); - return true; - } - case 7: { + break; + case 7: this->oscillating = value.as_bool(); - return true; - } - case 8: { + break; + case 8: this->has_direction = value.as_bool(); - return true; - } - case 9: { + break; + case 9: this->direction = static_cast(value.as_uint32()); - return true; - } - case 10: { + break; + case 10: this->has_speed_level = value.as_bool(); - return true; - } - case 11: { + break; + case 11: this->speed_level = value.as_int32(); - return true; - } - case 12: { + break; + case 12: this->has_preset_mode = value.as_bool(); - return true; - } - case 14: { + break; + case 14: this->device_id = value.as_uint32(); - return true; - } + break; default: return false; } + return true; } bool FanCommandRequest::decode_length(uint32_t field_id, ProtoLengthDelimited value) { switch (field_id) { - case 13: { + case 13: this->preset_mode = value.as_string(); - return true; - } + break; default: return false; } + return true; } bool FanCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) { switch (field_id) { - case 1: { + case 1: this->key = value.as_fixed32(); - return true; - } + break; default: return false; } + return true; } #endif #ifdef USE_LIGHT @@ -520,133 +500,108 @@ void LightStateResponse::calculate_size(uint32_t &total_size) const { } bool LightCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { - case 2: { + case 2: this->has_state = value.as_bool(); - return true; - } - case 3: { + break; + case 3: this->state = value.as_bool(); - return true; - } - case 4: { + break; + case 4: this->has_brightness = value.as_bool(); - return true; - } - case 22: { + break; + case 22: this->has_color_mode = value.as_bool(); - return true; - } - case 23: { + break; + case 23: this->color_mode = static_cast(value.as_uint32()); - return true; - } - case 20: { + break; + case 20: this->has_color_brightness = value.as_bool(); - return true; - } - case 6: { + break; + case 6: this->has_rgb = value.as_bool(); - return true; - } - case 10: { + break; + case 10: this->has_white = value.as_bool(); - return true; - } - case 12: { + break; + case 12: this->has_color_temperature = value.as_bool(); - return true; - } - case 24: { + break; + case 24: this->has_cold_white = value.as_bool(); - return true; - } - case 26: { + break; + case 26: this->has_warm_white = value.as_bool(); - return true; - } - case 14: { + break; + case 14: this->has_transition_length = value.as_bool(); - return true; - } - case 15: { + break; + case 15: this->transition_length = value.as_uint32(); - return true; - } - case 16: { + break; + case 16: this->has_flash_length = value.as_bool(); - return true; - } - case 17: { + break; + case 17: this->flash_length = value.as_uint32(); - return true; - } - case 18: { + break; + case 18: this->has_effect = value.as_bool(); - return true; - } - case 28: { + break; + case 28: this->device_id = value.as_uint32(); - return true; - } + break; default: return false; } + return true; } bool LightCommandRequest::decode_length(uint32_t field_id, ProtoLengthDelimited value) { switch (field_id) { - case 19: { + case 19: this->effect = value.as_string(); - return true; - } + break; default: return false; } + return true; } bool LightCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) { switch (field_id) { - case 1: { + case 1: this->key = value.as_fixed32(); - return true; - } - case 5: { + break; + case 5: this->brightness = value.as_float(); - return true; - } - case 21: { + break; + case 21: this->color_brightness = value.as_float(); - return true; - } - case 7: { + break; + case 7: this->red = value.as_float(); - return true; - } - case 8: { + break; + case 8: this->green = value.as_float(); - return true; - } - case 9: { + break; + case 9: this->blue = value.as_float(); - return true; - } - case 11: { + break; + case 11: this->white = value.as_float(); - return true; - } - case 13: { + break; + case 13: this->color_temperature = value.as_float(); - return true; - } - case 25: { + break; + case 25: this->cold_white = value.as_float(); - return true; - } - case 27: { + break; + case 27: this->warm_white = value.as_float(); - return true; - } + break; default: return false; } + return true; } #endif #ifdef USE_SENSOR @@ -732,27 +687,26 @@ void SwitchStateResponse::calculate_size(uint32_t &total_size) const { } bool SwitchCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { - case 2: { + case 2: this->state = value.as_bool(); - return true; - } - case 3: { + break; + case 3: this->device_id = value.as_uint32(); - return true; - } + break; default: return false; } + return true; } bool SwitchCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) { switch (field_id) { - case 1: { + case 1: this->key = value.as_fixed32(); - return true; - } + break; default: return false; } + return true; } #endif #ifdef USE_TEXT_SENSOR @@ -793,17 +747,16 @@ void TextSensorStateResponse::calculate_size(uint32_t &total_size) const { #endif bool SubscribeLogsRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { - case 1: { + case 1: this->level = static_cast(value.as_uint32()); - return true; - } - case 2: { + break; + case 2: this->dump_config = value.as_bool(); - return true; - } + break; default: return false; } + return true; } void SubscribeLogsResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_uint32(1, static_cast(this->level)); @@ -818,13 +771,13 @@ void SubscribeLogsResponse::calculate_size(uint32_t &total_size) const { #ifdef USE_API_NOISE bool NoiseEncryptionSetKeyRequest::decode_length(uint32_t field_id, ProtoLengthDelimited value) { switch (field_id) { - case 1: { + case 1: this->key = value.as_string(); - return true; - } + break; default: return false; } + return true; } void NoiseEncryptionSetKeyResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_bool(1, this->success); } void NoiseEncryptionSetKeyResponse::calculate_size(uint32_t &total_size) const { @@ -833,17 +786,16 @@ void NoiseEncryptionSetKeyResponse::calculate_size(uint32_t &total_size) const { #endif bool HomeassistantServiceMap::decode_length(uint32_t field_id, ProtoLengthDelimited value) { switch (field_id) { - case 1: { + case 1: this->key = value.as_string(); - return true; - } - case 2: { + break; + case 2: this->value = value.as_string(); - return true; - } + break; default: return false; } + return true; } void HomeassistantServiceMap::encode(ProtoWriteBuffer buffer) const { buffer.encode_string(1, this->key); @@ -885,31 +837,29 @@ void SubscribeHomeAssistantStateResponse::calculate_size(uint32_t &total_size) c } bool HomeAssistantStateResponse::decode_length(uint32_t field_id, ProtoLengthDelimited value) { switch (field_id) { - case 1: { + case 1: this->entity_id = value.as_string(); - return true; - } - case 2: { + break; + case 2: this->state = value.as_string(); - return true; - } - case 3: { + break; + case 3: this->attribute = value.as_string(); - return true; - } + break; default: return false; } + return true; } bool GetTimeResponse::decode_32bit(uint32_t field_id, Proto32Bit value) { switch (field_id) { - case 1: { + case 1: this->epoch_seconds = value.as_fixed32(); - return true; - } + break; default: return false; } + return true; } void GetTimeResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_fixed32(1, this->epoch_seconds); } void GetTimeResponse::calculate_size(uint32_t &total_size) const { @@ -918,23 +868,23 @@ void GetTimeResponse::calculate_size(uint32_t &total_size) const { #ifdef USE_API_SERVICES bool ListEntitiesServicesArgument::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { - case 2: { + case 2: this->type = static_cast(value.as_uint32()); - return true; - } + break; default: return false; } + return true; } bool ListEntitiesServicesArgument::decode_length(uint32_t field_id, ProtoLengthDelimited value) { switch (field_id) { - case 1: { + case 1: this->name = value.as_string(); - return true; - } + break; default: return false; } + return true; } void ListEntitiesServicesArgument::encode(ProtoWriteBuffer buffer) const { buffer.encode_string(1, this->name); @@ -958,57 +908,51 @@ void ListEntitiesServicesResponse::calculate_size(uint32_t &total_size) const { } bool ExecuteServiceArgument::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { - case 1: { + case 1: this->bool_ = value.as_bool(); - return true; - } - case 2: { + break; + case 2: this->legacy_int = value.as_int32(); - return true; - } - case 5: { + break; + case 5: this->int_ = value.as_sint32(); - return true; - } - case 6: { + break; + case 6: this->bool_array.push_back(value.as_bool()); - return true; - } - case 7: { + break; + case 7: this->int_array.push_back(value.as_sint32()); - return true; - } + break; default: return false; } + return true; } bool ExecuteServiceArgument::decode_length(uint32_t field_id, ProtoLengthDelimited value) { switch (field_id) { - case 4: { + case 4: this->string_ = value.as_string(); - return true; - } - case 9: { + break; + case 9: this->string_array.push_back(value.as_string()); - return true; - } + break; default: return false; } + return true; } bool ExecuteServiceArgument::decode_32bit(uint32_t field_id, Proto32Bit value) { switch (field_id) { - case 3: { + case 3: this->float_ = value.as_float(); - return true; - } - case 8: { + break; + case 8: this->float_array.push_back(value.as_float()); - return true; - } + break; default: return false; } + return true; } void ExecuteServiceArgument::encode(ProtoWriteBuffer buffer) const { buffer.encode_bool(1, this->bool_); @@ -1056,24 +1000,24 @@ void ExecuteServiceArgument::calculate_size(uint32_t &total_size) const { } bool ExecuteServiceRequest::decode_length(uint32_t field_id, ProtoLengthDelimited value) { switch (field_id) { - case 2: { + case 2: this->args.emplace_back(); value.decode_to_message(this->args.back()); - return true; - } + break; default: return false; } + return true; } bool ExecuteServiceRequest::decode_32bit(uint32_t field_id, Proto32Bit value) { switch (field_id) { - case 1: { + case 1: this->key = value.as_fixed32(); - return true; - } + break; default: return false; } + return true; } #endif #ifdef USE_CAMERA @@ -1111,17 +1055,16 @@ void CameraImageResponse::calculate_size(uint32_t &total_size) const { } bool CameraImageRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { - case 1: { + case 1: this->single = value.as_bool(); - return true; - } - case 2: { + break; + case 2: this->stream = value.as_bool(); - return true; - } + break; default: return false; } + return true; } #endif #ifdef USE_CLIMATE @@ -1255,117 +1198,96 @@ void ClimateStateResponse::calculate_size(uint32_t &total_size) const { } bool ClimateCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { - case 2: { + case 2: this->has_mode = value.as_bool(); - return true; - } - case 3: { + break; + case 3: this->mode = static_cast(value.as_uint32()); - return true; - } - case 4: { + break; + case 4: this->has_target_temperature = value.as_bool(); - return true; - } - case 6: { + break; + case 6: this->has_target_temperature_low = value.as_bool(); - return true; - } - case 8: { + break; + case 8: this->has_target_temperature_high = value.as_bool(); - return true; - } - case 10: { + break; + case 10: this->unused_has_legacy_away = value.as_bool(); - return true; - } - case 11: { + break; + case 11: this->unused_legacy_away = value.as_bool(); - return true; - } - case 12: { + break; + case 12: this->has_fan_mode = value.as_bool(); - return true; - } - case 13: { + break; + case 13: this->fan_mode = static_cast(value.as_uint32()); - return true; - } - case 14: { + break; + case 14: this->has_swing_mode = value.as_bool(); - return true; - } - case 15: { + break; + case 15: this->swing_mode = static_cast(value.as_uint32()); - return true; - } - case 16: { + break; + case 16: this->has_custom_fan_mode = value.as_bool(); - return true; - } - case 18: { + break; + case 18: this->has_preset = value.as_bool(); - return true; - } - case 19: { + break; + case 19: this->preset = static_cast(value.as_uint32()); - return true; - } - case 20: { + break; + case 20: this->has_custom_preset = value.as_bool(); - return true; - } - case 22: { + break; + case 22: this->has_target_humidity = value.as_bool(); - return true; - } - case 24: { + break; + case 24: this->device_id = value.as_uint32(); - return true; - } + break; default: return false; } + return true; } bool ClimateCommandRequest::decode_length(uint32_t field_id, ProtoLengthDelimited value) { switch (field_id) { - case 17: { + case 17: this->custom_fan_mode = value.as_string(); - return true; - } - case 21: { + break; + case 21: this->custom_preset = value.as_string(); - return true; - } + break; default: return false; } + return true; } bool ClimateCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) { switch (field_id) { - case 1: { + case 1: this->key = value.as_fixed32(); - return true; - } - case 5: { + break; + case 5: this->target_temperature = value.as_float(); - return true; - } - case 7: { + break; + case 7: this->target_temperature_low = value.as_float(); - return true; - } - case 9: { + break; + case 9: this->target_temperature_high = value.as_float(); - return true; - } - case 23: { + break; + case 23: this->target_humidity = value.as_float(); - return true; - } + break; default: return false; } + return true; } #endif #ifdef USE_NUMBER @@ -1415,27 +1337,26 @@ void NumberStateResponse::calculate_size(uint32_t &total_size) const { } bool NumberCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { - case 3: { + case 3: this->device_id = value.as_uint32(); - return true; - } + break; default: return false; } + return true; } bool NumberCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) { switch (field_id) { - case 1: { + case 1: this->key = value.as_fixed32(); - return true; - } - case 2: { + break; + case 2: this->state = value.as_float(); - return true; - } + break; default: return false; } + return true; } #endif #ifdef USE_SELECT @@ -1481,33 +1402,33 @@ void SelectStateResponse::calculate_size(uint32_t &total_size) const { } bool SelectCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { - case 3: { + case 3: this->device_id = value.as_uint32(); - return true; - } + break; default: return false; } + return true; } bool SelectCommandRequest::decode_length(uint32_t field_id, ProtoLengthDelimited value) { switch (field_id) { - case 2: { + case 2: this->state = value.as_string(); - return true; - } + break; default: return false; } + return true; } bool SelectCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) { switch (field_id) { - case 1: { + case 1: this->key = value.as_fixed32(); - return true; - } + break; default: return false; } + return true; } #endif #ifdef USE_SIREN @@ -1555,61 +1476,54 @@ void SirenStateResponse::calculate_size(uint32_t &total_size) const { } bool SirenCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { - case 2: { + case 2: this->has_state = value.as_bool(); - return true; - } - case 3: { + break; + case 3: this->state = value.as_bool(); - return true; - } - case 4: { + break; + case 4: this->has_tone = value.as_bool(); - return true; - } - case 6: { + break; + case 6: this->has_duration = value.as_bool(); - return true; - } - case 7: { + break; + case 7: this->duration = value.as_uint32(); - return true; - } - case 8: { + break; + case 8: this->has_volume = value.as_bool(); - return true; - } - case 10: { + break; + case 10: this->device_id = value.as_uint32(); - return true; - } + break; default: return false; } + return true; } bool SirenCommandRequest::decode_length(uint32_t field_id, ProtoLengthDelimited value) { switch (field_id) { - case 5: { + case 5: this->tone = value.as_string(); - return true; - } + break; default: return false; } + return true; } bool SirenCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) { switch (field_id) { - case 1: { + case 1: this->key = value.as_fixed32(); - return true; - } - case 9: { + break; + case 9: this->volume = value.as_float(); - return true; - } + break; default: return false; } + return true; } #endif #ifdef USE_LOCK @@ -1653,41 +1567,39 @@ void LockStateResponse::calculate_size(uint32_t &total_size) const { } bool LockCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { - case 2: { + case 2: this->command = static_cast(value.as_uint32()); - return true; - } - case 3: { + break; + case 3: this->has_code = value.as_bool(); - return true; - } - case 5: { + break; + case 5: this->device_id = value.as_uint32(); - return true; - } + break; default: return false; } + return true; } bool LockCommandRequest::decode_length(uint32_t field_id, ProtoLengthDelimited value) { switch (field_id) { - case 4: { + case 4: this->code = value.as_string(); - return true; - } + break; default: return false; } + return true; } bool LockCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) { switch (field_id) { - case 1: { + case 1: this->key = value.as_fixed32(); - return true; - } + break; default: return false; } + return true; } #endif #ifdef USE_BUTTON @@ -1715,57 +1627,54 @@ void ListEntitiesButtonResponse::calculate_size(uint32_t &total_size) const { } bool ButtonCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { - case 2: { + case 2: this->device_id = value.as_uint32(); - return true; - } + break; default: return false; } + return true; } bool ButtonCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) { switch (field_id) { - case 1: { + case 1: this->key = value.as_fixed32(); - return true; - } + break; default: return false; } + return true; } #endif #ifdef USE_MEDIA_PLAYER bool MediaPlayerSupportedFormat::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { - case 2: { + case 2: this->sample_rate = value.as_uint32(); - return true; - } - case 3: { + break; + case 3: this->num_channels = value.as_uint32(); - return true; - } - case 4: { + break; + case 4: this->purpose = static_cast(value.as_uint32()); - return true; - } - case 5: { + break; + case 5: this->sample_bytes = value.as_uint32(); - return true; - } + break; default: return false; } + return true; } bool MediaPlayerSupportedFormat::decode_length(uint32_t field_id, ProtoLengthDelimited value) { switch (field_id) { - case 1: { + case 1: this->format = value.as_string(); - return true; - } + break; default: return false; } + return true; } void MediaPlayerSupportedFormat::encode(ProtoWriteBuffer buffer) const { buffer.encode_string(1, this->format); @@ -1823,97 +1732,89 @@ void MediaPlayerStateResponse::calculate_size(uint32_t &total_size) const { } bool MediaPlayerCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { - case 2: { + case 2: this->has_command = value.as_bool(); - return true; - } - case 3: { + break; + case 3: this->command = static_cast(value.as_uint32()); - return true; - } - case 4: { + break; + case 4: this->has_volume = value.as_bool(); - return true; - } - case 6: { + break; + case 6: this->has_media_url = value.as_bool(); - return true; - } - case 8: { + break; + case 8: this->has_announcement = value.as_bool(); - return true; - } - case 9: { + break; + case 9: this->announcement = value.as_bool(); - return true; - } - case 10: { + break; + case 10: this->device_id = value.as_uint32(); - return true; - } + break; default: return false; } + return true; } bool MediaPlayerCommandRequest::decode_length(uint32_t field_id, ProtoLengthDelimited value) { switch (field_id) { - case 7: { + case 7: this->media_url = value.as_string(); - return true; - } + break; default: return false; } + return true; } bool MediaPlayerCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) { switch (field_id) { - case 1: { + case 1: this->key = value.as_fixed32(); - return true; - } - case 5: { + break; + case 5: this->volume = value.as_float(); - return true; - } + break; default: return false; } + return true; } #endif #ifdef USE_BLUETOOTH_PROXY bool SubscribeBluetoothLEAdvertisementsRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { - case 1: { + case 1: this->flags = value.as_uint32(); - return true; - } + break; default: return false; } + return true; } bool BluetoothServiceData::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { - case 2: { + case 2: this->legacy_data.push_back(value.as_uint32()); - return true; - } + break; default: return false; } + return true; } bool BluetoothServiceData::decode_length(uint32_t field_id, ProtoLengthDelimited value) { switch (field_id) { - case 1: { + case 1: this->uuid = value.as_string(); - return true; - } - case 3: { + break; + case 3: this->data = value.as_string(); - return true; - } + break; default: return false; } + return true; } void BluetoothServiceData::encode(ProtoWriteBuffer buffer) const { buffer.encode_string(1, this->uuid); @@ -1961,31 +1862,29 @@ void BluetoothLEAdvertisementResponse::calculate_size(uint32_t &total_size) cons } bool BluetoothLERawAdvertisement::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { - case 1: { + case 1: this->address = value.as_uint64(); - return true; - } - case 2: { + break; + case 2: this->rssi = value.as_sint32(); - return true; - } - case 3: { + break; + case 3: this->address_type = value.as_uint32(); - return true; - } + break; default: return false; } + return true; } bool BluetoothLERawAdvertisement::decode_length(uint32_t field_id, ProtoLengthDelimited value) { switch (field_id) { - case 4: { + case 4: this->data = value.as_string(); - return true; - } + break; default: return false; } + return true; } void BluetoothLERawAdvertisement::encode(ProtoWriteBuffer buffer) const { buffer.encode_uint64(1, this->address); @@ -2009,25 +1908,22 @@ void BluetoothLERawAdvertisementsResponse::calculate_size(uint32_t &total_size) } bool BluetoothDeviceRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { - case 1: { + case 1: this->address = value.as_uint64(); - return true; - } - case 2: { + break; + case 2: this->request_type = static_cast(value.as_uint32()); - return true; - } - case 3: { + break; + case 3: this->has_address_type = value.as_bool(); - return true; - } - case 4: { + break; + case 4: this->address_type = value.as_uint32(); - return true; - } + break; default: return false; } + return true; } void BluetoothDeviceConnectionResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_uint64(1, this->address); @@ -2043,27 +1939,26 @@ void BluetoothDeviceConnectionResponse::calculate_size(uint32_t &total_size) con } bool BluetoothGATTGetServicesRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { - case 1: { + case 1: this->address = value.as_uint64(); - return true; - } + break; default: return false; } + return true; } bool BluetoothGATTDescriptor::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { - case 1: { + case 1: this->uuid.push_back(value.as_uint64()); - return true; - } - case 2: { + break; + case 2: this->handle = value.as_uint32(); - return true; - } + break; default: return false; } + return true; } void BluetoothGATTDescriptor::encode(ProtoWriteBuffer buffer) const { for (auto &it : this->uuid) { @@ -2081,32 +1976,30 @@ void BluetoothGATTDescriptor::calculate_size(uint32_t &total_size) const { } bool BluetoothGATTCharacteristic::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { - case 1: { + case 1: this->uuid.push_back(value.as_uint64()); - return true; - } - case 2: { + break; + case 2: this->handle = value.as_uint32(); - return true; - } - case 3: { + break; + case 3: this->properties = value.as_uint32(); - return true; - } + break; default: return false; } + return true; } bool BluetoothGATTCharacteristic::decode_length(uint32_t field_id, ProtoLengthDelimited value) { switch (field_id) { - case 4: { + case 4: this->descriptors.emplace_back(); value.decode_to_message(this->descriptors.back()); - return true; - } + break; default: return false; } + return true; } void BluetoothGATTCharacteristic::encode(ProtoWriteBuffer buffer) const { for (auto &it : this->uuid) { @@ -2130,28 +2023,27 @@ void BluetoothGATTCharacteristic::calculate_size(uint32_t &total_size) const { } bool BluetoothGATTService::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { - case 1: { + case 1: this->uuid.push_back(value.as_uint64()); - return true; - } - case 2: { + break; + case 2: this->handle = value.as_uint32(); - return true; - } + break; default: return false; } + return true; } bool BluetoothGATTService::decode_length(uint32_t field_id, ProtoLengthDelimited value) { switch (field_id) { - case 3: { + case 3: this->characteristics.emplace_back(); value.decode_to_message(this->characteristics.back()); - return true; - } + break; default: return false; } + return true; } void BluetoothGATTService::encode(ProtoWriteBuffer buffer) const { for (auto &it : this->uuid) { @@ -2189,17 +2081,16 @@ void BluetoothGATTGetServicesDoneResponse::calculate_size(uint32_t &total_size) } bool BluetoothGATTReadRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { - case 1: { + case 1: this->address = value.as_uint64(); - return true; - } - case 2: { + break; + case 2: this->handle = value.as_uint32(); - return true; - } + break; default: return false; } + return true; } void BluetoothGATTReadResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_uint64(1, this->address); @@ -2213,87 +2104,81 @@ void BluetoothGATTReadResponse::calculate_size(uint32_t &total_size) const { } bool BluetoothGATTWriteRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { - case 1: { + case 1: this->address = value.as_uint64(); - return true; - } - case 2: { + break; + case 2: this->handle = value.as_uint32(); - return true; - } - case 3: { + break; + case 3: this->response = value.as_bool(); - return true; - } + break; default: return false; } + return true; } bool BluetoothGATTWriteRequest::decode_length(uint32_t field_id, ProtoLengthDelimited value) { switch (field_id) { - case 4: { + case 4: this->data = value.as_string(); - return true; - } + break; default: return false; } + return true; } bool BluetoothGATTReadDescriptorRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { - case 1: { + case 1: this->address = value.as_uint64(); - return true; - } - case 2: { + break; + case 2: this->handle = value.as_uint32(); - return true; - } + break; default: return false; } + return true; } bool BluetoothGATTWriteDescriptorRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { - case 1: { + case 1: this->address = value.as_uint64(); - return true; - } - case 2: { + break; + case 2: this->handle = value.as_uint32(); - return true; - } + break; default: return false; } + return true; } bool BluetoothGATTWriteDescriptorRequest::decode_length(uint32_t field_id, ProtoLengthDelimited value) { switch (field_id) { - case 3: { + case 3: this->data = value.as_string(); - return true; - } + break; default: return false; } + return true; } bool BluetoothGATTNotifyRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { - case 1: { + case 1: this->address = value.as_uint64(); - return true; - } - case 2: { + break; + case 2: this->handle = value.as_uint32(); - return true; - } - case 3: { + break; + case 3: this->enable = value.as_bool(); - return true; - } + break; default: return false; } + return true; } void BluetoothGATTNotifyDataResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_uint64(1, this->address); @@ -2387,53 +2272,51 @@ void BluetoothScannerStateResponse::calculate_size(uint32_t &total_size) const { } bool BluetoothScannerSetModeRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { - case 1: { + case 1: this->mode = static_cast(value.as_uint32()); - return true; - } + break; default: return false; } + return true; } #endif #ifdef USE_VOICE_ASSISTANT bool SubscribeVoiceAssistantRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { - case 1: { + case 1: this->subscribe = value.as_bool(); - return true; - } - case 2: { + break; + case 2: this->flags = value.as_uint32(); - return true; - } + break; default: return false; } + return true; } bool VoiceAssistantAudioSettings::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { - case 1: { + case 1: this->noise_suppression_level = value.as_uint32(); - return true; - } - case 2: { + break; + case 2: this->auto_gain = value.as_uint32(); - return true; - } + break; default: return false; } + return true; } bool VoiceAssistantAudioSettings::decode_32bit(uint32_t field_id, Proto32Bit value) { switch (field_id) { - case 3: { + case 3: this->volume_multiplier = value.as_float(); - return true; - } + break; default: return false; } + return true; } void VoiceAssistantAudioSettings::encode(ProtoWriteBuffer buffer) const { buffer.encode_uint32(1, this->noise_suppression_level); @@ -2461,31 +2344,29 @@ void VoiceAssistantRequest::calculate_size(uint32_t &total_size) const { } bool VoiceAssistantResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { - case 1: { + case 1: this->port = value.as_uint32(); - return true; - } - case 2: { + break; + case 2: this->error = value.as_bool(); - return true; - } + break; default: return false; } + return true; } bool VoiceAssistantEventData::decode_length(uint32_t field_id, ProtoLengthDelimited value) { switch (field_id) { - case 1: { + case 1: this->name = value.as_string(); - return true; - } - case 2: { + break; + case 2: this->value = value.as_string(); - return true; - } + break; default: return false; } + return true; } void VoiceAssistantEventData::encode(ProtoWriteBuffer buffer) const { buffer.encode_string(1, this->name); @@ -2497,44 +2378,44 @@ void VoiceAssistantEventData::calculate_size(uint32_t &total_size) const { } bool VoiceAssistantEventResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { - case 1: { + case 1: this->event_type = static_cast(value.as_uint32()); - return true; - } + break; default: return false; } + return true; } bool VoiceAssistantEventResponse::decode_length(uint32_t field_id, ProtoLengthDelimited value) { switch (field_id) { - case 2: { + case 2: this->data.emplace_back(); value.decode_to_message(this->data.back()); - return true; - } + break; default: return false; } + return true; } bool VoiceAssistantAudio::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { - case 2: { + case 2: this->end = value.as_bool(); - return true; - } + break; default: return false; } + return true; } bool VoiceAssistantAudio::decode_length(uint32_t field_id, ProtoLengthDelimited value) { switch (field_id) { - case 1: { + case 1: this->data = value.as_string(); - return true; - } + break; default: return false; } + return true; } void VoiceAssistantAudio::encode(ProtoWriteBuffer buffer) const { buffer.encode_bytes(1, reinterpret_cast(this->data.data()), this->data.size()); @@ -2546,67 +2427,61 @@ void VoiceAssistantAudio::calculate_size(uint32_t &total_size) const { } bool VoiceAssistantTimerEventResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { - case 1: { + case 1: this->event_type = static_cast(value.as_uint32()); - return true; - } - case 4: { + break; + case 4: this->total_seconds = value.as_uint32(); - return true; - } - case 5: { + break; + case 5: this->seconds_left = value.as_uint32(); - return true; - } - case 6: { + break; + case 6: this->is_active = value.as_bool(); - return true; - } + break; default: return false; } + return true; } bool VoiceAssistantTimerEventResponse::decode_length(uint32_t field_id, ProtoLengthDelimited value) { switch (field_id) { - case 2: { + case 2: this->timer_id = value.as_string(); - return true; - } - case 3: { + break; + case 3: this->name = value.as_string(); - return true; - } + break; default: return false; } + return true; } bool VoiceAssistantAnnounceRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { - case 4: { + case 4: this->start_conversation = value.as_bool(); - return true; - } + break; default: return false; } + return true; } bool VoiceAssistantAnnounceRequest::decode_length(uint32_t field_id, ProtoLengthDelimited value) { switch (field_id) { - case 1: { + case 1: this->media_id = value.as_string(); - return true; - } - case 2: { + break; + case 2: this->text = value.as_string(); - return true; - } - case 3: { + break; + case 3: this->preannounce_media_id = value.as_string(); - return true; - } + break; default: return false; } + return true; } void VoiceAssistantAnnounceFinished::encode(ProtoWriteBuffer buffer) const { buffer.encode_bool(1, this->success); } void VoiceAssistantAnnounceFinished::calculate_size(uint32_t &total_size) const { @@ -2614,21 +2489,19 @@ void VoiceAssistantAnnounceFinished::calculate_size(uint32_t &total_size) const } bool VoiceAssistantWakeWord::decode_length(uint32_t field_id, ProtoLengthDelimited value) { switch (field_id) { - case 1: { + case 1: this->id = value.as_string(); - return true; - } - case 2: { + break; + case 2: this->wake_word = value.as_string(); - return true; - } - case 3: { + break; + case 3: this->trained_languages.push_back(value.as_string()); - return true; - } + break; default: return false; } + return true; } void VoiceAssistantWakeWord::encode(ProtoWriteBuffer buffer) const { buffer.encode_string(1, this->id); @@ -2666,13 +2539,13 @@ void VoiceAssistantConfigurationResponse::calculate_size(uint32_t &total_size) c } bool VoiceAssistantSetConfiguration::decode_length(uint32_t field_id, ProtoLengthDelimited value) { switch (field_id) { - case 1: { + case 1: this->active_wake_words.push_back(value.as_string()); - return true; - } + break; default: return false; } + return true; } #endif #ifdef USE_ALARM_CONTROL_PANEL @@ -2714,37 +2587,36 @@ void AlarmControlPanelStateResponse::calculate_size(uint32_t &total_size) const } bool AlarmControlPanelCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { - case 2: { + case 2: this->command = static_cast(value.as_uint32()); - return true; - } - case 4: { + break; + case 4: this->device_id = value.as_uint32(); - return true; - } + break; default: return false; } + return true; } bool AlarmControlPanelCommandRequest::decode_length(uint32_t field_id, ProtoLengthDelimited value) { switch (field_id) { - case 3: { + case 3: this->code = value.as_string(); - return true; - } + break; default: return false; } + return true; } bool AlarmControlPanelCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) { switch (field_id) { - case 1: { + case 1: this->key = value.as_fixed32(); - return true; - } + break; default: return false; } + return true; } #endif #ifdef USE_TEXT @@ -2790,33 +2662,33 @@ void TextStateResponse::calculate_size(uint32_t &total_size) const { } bool TextCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { - case 3: { + case 3: this->device_id = value.as_uint32(); - return true; - } + break; default: return false; } + return true; } bool TextCommandRequest::decode_length(uint32_t field_id, ProtoLengthDelimited value) { switch (field_id) { - case 2: { + case 2: this->state = value.as_string(); - return true; - } + break; default: return false; } + return true; } bool TextCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) { switch (field_id) { - case 1: { + case 1: this->key = value.as_fixed32(); - return true; - } + break; default: return false; } + return true; } #endif #ifdef USE_DATETIME_DATE @@ -2858,35 +2730,32 @@ void DateStateResponse::calculate_size(uint32_t &total_size) const { } bool DateCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { - case 2: { + case 2: this->year = value.as_uint32(); - return true; - } - case 3: { + break; + case 3: this->month = value.as_uint32(); - return true; - } - case 4: { + break; + case 4: this->day = value.as_uint32(); - return true; - } - case 5: { + break; + case 5: this->device_id = value.as_uint32(); - return true; - } + break; default: return false; } + return true; } bool DateCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) { switch (field_id) { - case 1: { + case 1: this->key = value.as_fixed32(); - return true; - } + break; default: return false; } + return true; } #endif #ifdef USE_DATETIME_TIME @@ -2928,35 +2797,32 @@ void TimeStateResponse::calculate_size(uint32_t &total_size) const { } bool TimeCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { - case 2: { + case 2: this->hour = value.as_uint32(); - return true; - } - case 3: { + break; + case 3: this->minute = value.as_uint32(); - return true; - } - case 4: { + break; + case 4: this->second = value.as_uint32(); - return true; - } - case 5: { + break; + case 5: this->device_id = value.as_uint32(); - return true; - } + break; default: return false; } + return true; } bool TimeCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) { switch (field_id) { - case 1: { + case 1: this->key = value.as_fixed32(); - return true; - } + break; default: return false; } + return true; } #endif #ifdef USE_EVENT @@ -3044,35 +2910,32 @@ void ValveStateResponse::calculate_size(uint32_t &total_size) const { } bool ValveCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { - case 2: { + case 2: this->has_position = value.as_bool(); - return true; - } - case 4: { + break; + case 4: this->stop = value.as_bool(); - return true; - } - case 5: { + break; + case 5: this->device_id = value.as_uint32(); - return true; - } + break; default: return false; } + return true; } bool ValveCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) { switch (field_id) { - case 1: { + case 1: this->key = value.as_fixed32(); - return true; - } - case 3: { + break; + case 3: this->position = value.as_float(); - return true; - } + break; default: return false; } + return true; } #endif #ifdef USE_DATETIME_DATETIME @@ -3110,27 +2973,26 @@ void DateTimeStateResponse::calculate_size(uint32_t &total_size) const { } bool DateTimeCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { - case 3: { + case 3: this->device_id = value.as_uint32(); - return true; - } + break; default: return false; } + return true; } bool DateTimeCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) { switch (field_id) { - case 1: { + case 1: this->key = value.as_fixed32(); - return true; - } - case 2: { + break; + case 2: this->epoch_seconds = value.as_fixed32(); - return true; - } + break; default: return false; } + return true; } #endif #ifdef USE_UPDATE @@ -3184,27 +3046,26 @@ void UpdateStateResponse::calculate_size(uint32_t &total_size) const { } bool UpdateCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { - case 2: { + case 2: this->command = static_cast(value.as_uint32()); - return true; - } - case 3: { + break; + case 3: this->device_id = value.as_uint32(); - return true; - } + break; default: return false; } + return true; } bool UpdateCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) { switch (field_id) { - case 1: { + case 1: this->key = value.as_fixed32(); - return true; - } + break; default: return false; } + return true; } #endif diff --git a/script/api_protobuf/api_protobuf.py b/script/api_protobuf/api_protobuf.py index 01135bd63d..f6e18d529d 100755 --- a/script/api_protobuf/api_protobuf.py +++ b/script/api_protobuf/api_protobuf.py @@ -8,7 +8,6 @@ from pathlib import Path import re from subprocess import call import sys -from textwrap import dedent from typing import Any import aioesphomeapi.api_options_pb2 as pb @@ -157,13 +156,7 @@ class TypeInfo(ABC): content = self.decode_varint if content is None: return None - return dedent( - f"""\ - case {self.number}: {{ - this->{self.field_name} = {content}; - return true; - }}""" - ) + return f"case {self.number}: this->{self.field_name} = {content}; break;" decode_varint = None @@ -172,13 +165,7 @@ class TypeInfo(ABC): content = self.decode_length if content is None: return None - return dedent( - f"""\ - case {self.number}: {{ - this->{self.field_name} = {content}; - return true; - }}""" - ) + return f"case {self.number}: this->{self.field_name} = {content}; break;" decode_length = None @@ -187,13 +174,7 @@ class TypeInfo(ABC): content = self.decode_32bit if content is None: return None - return dedent( - f"""\ - case {self.number}: {{ - this->{self.field_name} = {content}; - return true; - }}""" - ) + return f"case {self.number}: this->{self.field_name} = {content}; break;" decode_32bit = None @@ -202,13 +183,7 @@ class TypeInfo(ABC): content = self.decode_64bit if content is None: return None - return dedent( - f"""\ - case {self.number}: {{ - this->{self.field_name} = {content}; - return true; - }}""" - ) + return f"case {self.number}: this->{self.field_name} = {content}; break;" decode_64bit = None @@ -580,13 +555,7 @@ class MessageType(TypeInfo): @property def decode_length_content(self) -> str: # Custom decode that doesn't use templates - return dedent( - f"""\ - case {self.number}: {{ - value.decode_to_message(this->{self.field_name}); - return true; - }}""" - ) + return f"case {self.number}: value.decode_to_message(this->{self.field_name}); break;" def dump(self, name: str) -> str: o = f"{name}.dump_to(out);" @@ -797,12 +766,8 @@ class RepeatedTypeInfo(TypeInfo): content = self._ti.decode_varint if content is None: return None - return dedent( - f"""\ - case {self.number}: {{ - this->{self.field_name}.push_back({content}); - return true; - }}""" + return ( + f"case {self.number}: this->{self.field_name}.push_back({content}); break;" ) @property @@ -810,22 +775,11 @@ class RepeatedTypeInfo(TypeInfo): content = self._ti.decode_length if content is None and isinstance(self._ti, MessageType): # Special handling for non-template message decoding - return dedent( - f"""\ - case {self.number}: {{ - this->{self.field_name}.emplace_back(); - value.decode_to_message(this->{self.field_name}.back()); - return true; - }}""" - ) + return f"case {self.number}: this->{self.field_name}.emplace_back(); value.decode_to_message(this->{self.field_name}.back()); break;" if content is None: return None - return dedent( - f"""\ - case {self.number}: {{ - this->{self.field_name}.push_back({content}); - return true; - }}""" + return ( + f"case {self.number}: this->{self.field_name}.push_back({content}); break;" ) @property @@ -833,12 +787,8 @@ class RepeatedTypeInfo(TypeInfo): content = self._ti.decode_32bit if content is None: return None - return dedent( - f"""\ - case {self.number}: {{ - this->{self.field_name}.push_back({content}); - return true; - }}""" + return ( + f"case {self.number}: this->{self.field_name}.push_back({content}); break;" ) @property @@ -846,12 +796,8 @@ class RepeatedTypeInfo(TypeInfo): content = self._ti.decode_64bit if content is None: return None - return dedent( - f"""\ - case {self.number}: {{ - this->{self.field_name}.push_back({content}); - return true; - }}""" + return ( + f"case {self.number}: this->{self.field_name}.push_back({content}); break;" ) @property @@ -1155,41 +1101,45 @@ def build_message_type( cpp = "" if decode_varint: - decode_varint.append("default:\n return false;") o = f"bool {desc.name}::decode_varint(uint32_t field_id, ProtoVarInt value) {{\n" o += " switch (field_id) {\n" o += indent("\n".join(decode_varint), " ") + "\n" + o += " default: return false;\n" o += " }\n" + o += " return true;\n" o += "}\n" cpp += o prot = "bool decode_varint(uint32_t field_id, ProtoVarInt value) override;" protected_content.insert(0, prot) if decode_length: - decode_length.append("default:\n return false;") o = f"bool {desc.name}::decode_length(uint32_t field_id, ProtoLengthDelimited value) {{\n" o += " switch (field_id) {\n" o += indent("\n".join(decode_length), " ") + "\n" + o += " default: return false;\n" o += " }\n" + o += " return true;\n" o += "}\n" cpp += o prot = "bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override;" protected_content.insert(0, prot) if decode_32bit: - decode_32bit.append("default:\n return false;") o = f"bool {desc.name}::decode_32bit(uint32_t field_id, Proto32Bit value) {{\n" o += " switch (field_id) {\n" o += indent("\n".join(decode_32bit), " ") + "\n" + o += " default: return false;\n" o += " }\n" + o += " return true;\n" o += "}\n" cpp += o prot = "bool decode_32bit(uint32_t field_id, Proto32Bit value) override;" protected_content.insert(0, prot) if decode_64bit: - decode_64bit.append("default:\n return false;") o = f"bool {desc.name}::decode_64bit(uint32_t field_id, Proto64Bit value) {{\n" o += " switch (field_id) {\n" o += indent("\n".join(decode_64bit), " ") + "\n" + o += " default: return false;\n" o += " }\n" + o += " return true;\n" o += "}\n" cpp += o prot = "bool decode_64bit(uint32_t field_id, Proto64Bit value) override;" From bfaf2547e32de0108893c13c4bbe6cc55d6136c6 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 15 Jul 2025 15:15:23 -1000 Subject: [PATCH 05/14] Reduce API component flash usage by consolidating error logging (#9468) --- esphome/components/api/api_connection.cpp | 38 +++++++---------------- esphome/components/api/api_server.cpp | 2 +- 2 files changed, 13 insertions(+), 27 deletions(-) diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index ca5d3a97ba..f935518dbc 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -86,8 +86,8 @@ void APIConnection::start() { APIError err = this->helper_->init(); if (err != APIError::OK) { on_fatal_error(); - ESP_LOGW(TAG, "%s: Helper init failed: %s errno=%d", this->get_client_combined_info().c_str(), - api_error_to_str(err), errno); + ESP_LOGW(TAG, "%s: Helper init failed %s errno=%d", this->get_client_combined_info().c_str(), api_error_to_str(err), + errno); return; } this->client_info_ = helper_->getpeername(); @@ -119,7 +119,7 @@ void APIConnection::loop() { APIError err = this->helper_->loop(); if (err != APIError::OK) { on_fatal_error(); - ESP_LOGW(TAG, "%s: Socket operation failed: %s errno=%d", this->get_client_combined_info().c_str(), + ESP_LOGW(TAG, "%s: Socket operation failed %s errno=%d", this->get_client_combined_info().c_str(), api_error_to_str(err), errno); return; } @@ -136,14 +136,8 @@ void APIConnection::loop() { break; } else if (err != APIError::OK) { on_fatal_error(); - if (err == APIError::SOCKET_READ_FAILED && errno == ECONNRESET) { - ESP_LOGW(TAG, "%s: Connection reset", this->get_client_combined_info().c_str()); - } else if (err == APIError::CONNECTION_CLOSED) { - ESP_LOGW(TAG, "%s: Connection closed", this->get_client_combined_info().c_str()); - } else { - ESP_LOGW(TAG, "%s: Reading failed: %s errno=%d", this->get_client_combined_info().c_str(), - api_error_to_str(err), errno); - } + ESP_LOGW(TAG, "%s: Reading failed %s errno=%d", this->get_client_combined_info().c_str(), api_error_to_str(err), + errno); return; } else { this->last_traffic_ = now; @@ -1612,7 +1606,7 @@ bool APIConnection::try_to_clear_buffer(bool log_out_of_space) { APIError err = this->helper_->loop(); if (err != APIError::OK) { on_fatal_error(); - ESP_LOGW(TAG, "%s: Socket operation failed: %s errno=%d", this->get_client_combined_info().c_str(), + ESP_LOGW(TAG, "%s: Socket operation failed %s errno=%d", this->get_client_combined_info().c_str(), api_error_to_str(err), errno); return false; } @@ -1633,12 +1627,8 @@ bool APIConnection::send_buffer(ProtoWriteBuffer buffer, uint8_t message_type) { return false; if (err != APIError::OK) { on_fatal_error(); - if (err == APIError::SOCKET_WRITE_FAILED && errno == ECONNRESET) { - ESP_LOGW(TAG, "%s: Connection reset", this->get_client_combined_info().c_str()); - } else { - ESP_LOGW(TAG, "%s: Packet write failed %s errno=%d", this->get_client_combined_info().c_str(), - api_error_to_str(err), errno); - } + ESP_LOGW(TAG, "%s: Packet write failed %s errno=%d", this->get_client_combined_info().c_str(), + api_error_to_str(err), errno); return false; } // Do not set last_traffic_ on send @@ -1646,11 +1636,11 @@ bool APIConnection::send_buffer(ProtoWriteBuffer buffer, uint8_t message_type) { } void APIConnection::on_unauthenticated_access() { this->on_fatal_error(); - ESP_LOGD(TAG, "%s requested access without authentication", this->get_client_combined_info().c_str()); + ESP_LOGD(TAG, "%s access without authentication", this->get_client_combined_info().c_str()); } void APIConnection::on_no_setup_connection() { this->on_fatal_error(); - ESP_LOGD(TAG, "%s requested access without full connection", this->get_client_combined_info().c_str()); + ESP_LOGD(TAG, "%s access without full connection", this->get_client_combined_info().c_str()); } void APIConnection::on_fatal_error() { this->helper_->close(); @@ -1815,12 +1805,8 @@ void APIConnection::process_batch_() { this->helper_->write_protobuf_packets(ProtoWriteBuffer{&this->parent_->get_shared_buffer_ref()}, packet_info); if (err != APIError::OK && err != APIError::WOULD_BLOCK) { on_fatal_error(); - if (err == APIError::SOCKET_WRITE_FAILED && errno == ECONNRESET) { - ESP_LOGW(TAG, "%s: Connection reset during batch write", this->get_client_combined_info().c_str()); - } else { - ESP_LOGW(TAG, "%s: Batch write failed %s errno=%d", this->get_client_combined_info().c_str(), - api_error_to_str(err), errno); - } + ESP_LOGW(TAG, "%s: Batch write failed %s errno=%d", this->get_client_combined_info().c_str(), api_error_to_str(err), + errno); } #ifdef HAS_PROTO_MESSAGE_DUMP diff --git a/esphome/components/api/api_server.cpp b/esphome/components/api/api_server.cpp index 5b87a773b5..750143d7f1 100644 --- a/esphome/components/api/api_server.cpp +++ b/esphome/components/api/api_server.cpp @@ -426,7 +426,7 @@ bool APIServer::save_noise_psk(psk_t psk, bool make_active) { ESP_LOGD(TAG, "Noise PSK saved"); if (make_active) { this->set_timeout(100, [this, psk]() { - ESP_LOGW(TAG, "Disconnecting all clients to reset connections"); + ESP_LOGW(TAG, "Disconnecting all clients to reset PSK"); this->set_noise_psk(psk); for (auto &c : this->clients_) { c->send_message(DisconnectRequest()); From 30c4b9169739503565f160839003d2f786b80e04 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 15 Jul 2025 15:19:03 -1000 Subject: [PATCH 06/14] Remove parsed advertisement support from bluetooth_proxy to save memory (#9489) --- .../components/bluetooth_proxy/__init__.py | 4 +- .../bluetooth_proxy/bluetooth_proxy.cpp | 41 +++++------ .../bluetooth_proxy/bluetooth_proxy.h | 7 +- .../esp32_ble_client/ble_client_base.cpp | 2 + .../esp32_ble_client/ble_client_base.h | 2 + .../components/esp32_ble_tracker/__init__.py | 69 ++++++++++++++++++- .../components/esp32_ble_tracker/automation.h | 2 + .../esp32_ble_tracker/esp32_ble_tracker.cpp | 10 ++- .../esp32_ble_tracker/esp32_ble_tracker.h | 6 ++ esphome/core/defines.h | 1 + 10 files changed, 113 insertions(+), 31 deletions(-) diff --git a/esphome/components/bluetooth_proxy/__init__.py b/esphome/components/bluetooth_proxy/__init__.py index 5c144cadcc..a1e9d464df 100644 --- a/esphome/components/bluetooth_proxy/__init__.py +++ b/esphome/components/bluetooth_proxy/__init__.py @@ -85,13 +85,13 @@ async def to_code(config): await cg.register_component(var, config) cg.add(var.set_active(config[CONF_ACTIVE])) - await esp32_ble_tracker.register_ble_device(var, config) + await esp32_ble_tracker.register_raw_ble_device(var, config) for connection_conf in config.get(CONF_CONNECTIONS, []): connection_var = cg.new_Pvariable(connection_conf[CONF_ID]) await cg.register_component(connection_var, connection_conf) cg.add(var.register_connection(connection_var)) - await esp32_ble_tracker.register_client(connection_var, connection_conf) + await esp32_ble_tracker.register_raw_client(connection_var, connection_conf) if config.get(CONF_CACHE_SERVICES): add_idf_sdkconfig_option("CONFIG_BT_GATTC_CACHE_NVS_FLASH", True) diff --git a/esphome/components/bluetooth_proxy/bluetooth_proxy.cpp b/esphome/components/bluetooth_proxy/bluetooth_proxy.cpp index a5e8ec0860..1c856b8d93 100644 --- a/esphome/components/bluetooth_proxy/bluetooth_proxy.cpp +++ b/esphome/components/bluetooth_proxy/bluetooth_proxy.cpp @@ -42,15 +42,13 @@ void BluetoothProxy::send_bluetooth_scanner_state_(esp32_ble_tracker::ScannerSta this->api_connection_->send_message(resp); } +#ifdef USE_ESP32_BLE_DEVICE bool BluetoothProxy::parse_device(const esp32_ble_tracker::ESPBTDevice &device) { - if (!api::global_api_server->is_connected() || this->api_connection_ == nullptr || this->raw_advertisements_) - return false; - - ESP_LOGV(TAG, "Proxying packet from %s - %s. RSSI: %d dB", device.get_name().c_str(), device.address_str().c_str(), - device.get_rssi()); - this->send_api_packet_(device); - return true; + // This method should never be called since bluetooth_proxy always uses raw advertisements + // but we need to provide an implementation to satisfy the virtual method requirement + return false; } +#endif // Batch size for BLE advertisements to maximize WiFi efficiency // Each advertisement is up to 80 bytes when packaged (including protocol overhead) @@ -69,7 +67,7 @@ std::vector batch_buffer; static std::vector &get_batch_buffer() { return batch_buffer; } bool BluetoothProxy::parse_devices(const esp32_ble::BLEScanResult *scan_results, size_t count) { - if (!api::global_api_server->is_connected() || this->api_connection_ == nullptr || !this->raw_advertisements_) + if (!api::global_api_server->is_connected() || this->api_connection_ == nullptr) return false; // Get the batch buffer reference @@ -116,6 +114,7 @@ void BluetoothProxy::flush_pending_advertisements() { this->api_connection_->send_message(resp); } +#ifdef USE_ESP32_BLE_DEVICE void BluetoothProxy::send_api_packet_(const esp32_ble_tracker::ESPBTDevice &device) { api::BluetoothLEAdvertisementResponse resp; resp.address = device.address_uint64(); @@ -153,14 +152,14 @@ void BluetoothProxy::send_api_packet_(const esp32_ble_tracker::ESPBTDevice &devi this->api_connection_->send_message(resp); } +#endif // USE_ESP32_BLE_DEVICE void BluetoothProxy::dump_config() { ESP_LOGCONFIG(TAG, "Bluetooth Proxy:"); ESP_LOGCONFIG(TAG, " Active: %s\n" - " Connections: %d\n" - " Raw advertisements: %s", - YESNO(this->active_), this->connections_.size(), YESNO(this->raw_advertisements_)); + " Connections: %d", + YESNO(this->active_), this->connections_.size()); } int BluetoothProxy::get_bluetooth_connections_free() { @@ -188,15 +187,13 @@ void BluetoothProxy::loop() { } // Flush any pending BLE advertisements that have been accumulated but not yet sent - if (this->raw_advertisements_) { - static uint32_t last_flush_time = 0; - uint32_t now = App.get_loop_component_start_time(); + static uint32_t last_flush_time = 0; + uint32_t now = App.get_loop_component_start_time(); - // Flush accumulated advertisements every 100ms - if (now - last_flush_time >= 100) { - this->flush_pending_advertisements(); - last_flush_time = now; - } + // Flush accumulated advertisements every 100ms + if (now - last_flush_time >= 100) { + this->flush_pending_advertisements(); + last_flush_time = now; } for (auto *connection : this->connections_) { if (connection->send_service_ == connection->service_count_) { @@ -318,9 +315,7 @@ void BluetoothProxy::loop() { } esp32_ble_tracker::AdvertisementParserType BluetoothProxy::get_advertisement_parser_type() { - if (this->raw_advertisements_) - return esp32_ble_tracker::AdvertisementParserType::RAW_ADVERTISEMENTS; - return esp32_ble_tracker::AdvertisementParserType::PARSED_ADVERTISEMENTS; + return esp32_ble_tracker::AdvertisementParserType::RAW_ADVERTISEMENTS; } BluetoothConnection *BluetoothProxy::get_connection_(uint64_t address, bool reserve) { @@ -565,7 +560,6 @@ void BluetoothProxy::subscribe_api_connection(api::APIConnection *api_connection return; } this->api_connection_ = api_connection; - this->raw_advertisements_ = flags & BluetoothProxySubscriptionFlag::SUBSCRIPTION_RAW_ADVERTISEMENTS; this->parent_->recalculate_advertisement_parser_types(); this->send_bluetooth_scanner_state_(this->parent_->get_scanner_state()); @@ -577,7 +571,6 @@ void BluetoothProxy::unsubscribe_api_connection(api::APIConnection *api_connecti return; } this->api_connection_ = nullptr; - this->raw_advertisements_ = false; this->parent_->recalculate_advertisement_parser_types(); } diff --git a/esphome/components/bluetooth_proxy/bluetooth_proxy.h b/esphome/components/bluetooth_proxy/bluetooth_proxy.h index f0632350e0..3ccf0706a7 100644 --- a/esphome/components/bluetooth_proxy/bluetooth_proxy.h +++ b/esphome/components/bluetooth_proxy/bluetooth_proxy.h @@ -51,7 +51,9 @@ enum BluetoothProxySubscriptionFlag : uint32_t { class BluetoothProxy : public esp32_ble_tracker::ESPBTDeviceListener, public Component { public: BluetoothProxy(); +#ifdef USE_ESP32_BLE_DEVICE bool parse_device(const esp32_ble_tracker::ESPBTDevice &device) override; +#endif bool parse_devices(const esp32_ble::BLEScanResult *scan_results, size_t count) override; void dump_config() override; void setup() override; @@ -129,7 +131,9 @@ class BluetoothProxy : public esp32_ble_tracker::ESPBTDeviceListener, public Com } protected: +#ifdef USE_ESP32_BLE_DEVICE void send_api_packet_(const esp32_ble_tracker::ESPBTDevice &device); +#endif void send_bluetooth_scanner_state_(esp32_ble_tracker::ScannerState state); BluetoothConnection *get_connection_(uint64_t address, bool reserve); @@ -143,8 +147,7 @@ class BluetoothProxy : public esp32_ble_tracker::ESPBTDeviceListener, public Com // Group 3: 1-byte types grouped together bool active_; - bool raw_advertisements_{false}; - // 2 bytes used, 2 bytes padding + // 1 byte used, 3 bytes padding }; extern BluetoothProxy *global_bluetooth_proxy; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) diff --git a/esphome/components/esp32_ble_client/ble_client_base.cpp b/esphome/components/esp32_ble_client/ble_client_base.cpp index 7d0a3bbfd5..bf425b3730 100644 --- a/esphome/components/esp32_ble_client/ble_client_base.cpp +++ b/esphome/components/esp32_ble_client/ble_client_base.cpp @@ -105,6 +105,7 @@ void BLEClientBase::dump_config() { } } +#ifdef USE_ESP32_BLE_DEVICE bool BLEClientBase::parse_device(const espbt::ESPBTDevice &device) { if (!this->auto_connect_) return false; @@ -122,6 +123,7 @@ bool BLEClientBase::parse_device(const espbt::ESPBTDevice &device) { this->remote_addr_type_ = device.get_address_type(); return true; } +#endif void BLEClientBase::connect() { ESP_LOGI(TAG, "[%d] [%s] 0x%02x Attempting BLE connection", this->connection_index_, this->address_str_.c_str(), diff --git a/esphome/components/esp32_ble_client/ble_client_base.h b/esphome/components/esp32_ble_client/ble_client_base.h index bf3b589b1b..457a88ec1d 100644 --- a/esphome/components/esp32_ble_client/ble_client_base.h +++ b/esphome/components/esp32_ble_client/ble_client_base.h @@ -31,7 +31,9 @@ class BLEClientBase : public espbt::ESPBTClient, public Component { void dump_config() override; void run_later(std::function &&f); // NOLINT +#ifdef USE_ESP32_BLE_DEVICE bool parse_device(const espbt::ESPBTDevice &device) override; +#endif void on_scan_end() override {} bool gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, esp_ble_gattc_cb_param_t *param) override; diff --git a/esphome/components/esp32_ble_tracker/__init__.py b/esphome/components/esp32_ble_tracker/__init__.py index 547cf84ed1..68f4657515 100644 --- a/esphome/components/esp32_ble_tracker/__init__.py +++ b/esphome/components/esp32_ble_tracker/__init__.py @@ -31,6 +31,8 @@ from esphome.const import ( CONF_TRIGGER_ID, ) from esphome.core import CORE +from esphome.enum import StrEnum +from esphome.types import ConfigType AUTO_LOAD = ["esp32_ble"] DEPENDENCIES = ["esp32"] @@ -50,6 +52,25 @@ IDF_MAX_CONNECTIONS = 9 _LOGGER = logging.getLogger(__name__) + +# Enum for BLE features +class BLEFeatures(StrEnum): + ESP_BT_DEVICE = "ESP_BT_DEVICE" + + +# Set to track which features are needed by components +_required_features: set[BLEFeatures] = set() + + +def register_ble_features(features: set[BLEFeatures]) -> None: + """Register BLE features that a component needs. + + Args: + features: Set of BLEFeatures enum members + """ + _required_features.update(features) + + esp32_ble_tracker_ns = cg.esphome_ns.namespace("esp32_ble_tracker") ESP32BLETracker = esp32_ble_tracker_ns.class_( "ESP32BLETracker", @@ -277,6 +298,15 @@ async def to_code(config): cg.add(var.set_scan_window(int(params[CONF_WINDOW].total_milliseconds / 0.625))) cg.add(var.set_scan_active(params[CONF_ACTIVE])) cg.add(var.set_scan_continuous(params[CONF_CONTINUOUS])) + + # Register ESP_BT_DEVICE feature if any of the automation triggers are used + if ( + config.get(CONF_ON_BLE_ADVERTISE) + or config.get(CONF_ON_BLE_SERVICE_DATA_ADVERTISE) + or config.get(CONF_ON_BLE_MANUFACTURER_DATA_ADVERTISE) + ): + register_ble_features({BLEFeatures.ESP_BT_DEVICE}) + for conf in config.get(CONF_ON_BLE_ADVERTISE, []): trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) if CONF_MAC_ADDRESS in conf: @@ -334,6 +364,11 @@ async def to_code(config): cg.add_define("USE_OTA_STATE_CALLBACK") # To be notified when an OTA update starts cg.add_define("USE_ESP32_BLE_CLIENT") + + # Add feature-specific defines based on what's needed + if BLEFeatures.ESP_BT_DEVICE in _required_features: + cg.add_define("USE_ESP32_BLE_DEVICE") + if config.get(CONF_SOFTWARE_COEXISTENCE): cg.add_define("USE_ESP32_BLE_SOFTWARE_COEXISTENCE") @@ -382,13 +417,43 @@ async def esp32_ble_tracker_stop_scan_action_to_code( return var -async def register_ble_device(var, config): +async def register_ble_device( + var: cg.SafeExpType, config: ConfigType +) -> cg.SafeExpType: + register_ble_features({BLEFeatures.ESP_BT_DEVICE}) paren = await cg.get_variable(config[CONF_ESP32_BLE_ID]) cg.add(paren.register_listener(var)) return var -async def register_client(var, config): +async def register_client(var: cg.SafeExpType, config: ConfigType) -> cg.SafeExpType: + register_ble_features({BLEFeatures.ESP_BT_DEVICE}) + paren = await cg.get_variable(config[CONF_ESP32_BLE_ID]) + cg.add(paren.register_client(var)) + return var + + +async def register_raw_ble_device( + var: cg.SafeExpType, config: ConfigType +) -> cg.SafeExpType: + """Register a BLE device listener that only needs raw advertisement data. + + This does NOT register the ESP_BT_DEVICE feature, meaning ESPBTDevice + will not be compiled in if this is the only registration method used. + """ + paren = await cg.get_variable(config[CONF_ESP32_BLE_ID]) + cg.add(paren.register_listener(var)) + return var + + +async def register_raw_client( + var: cg.SafeExpType, config: ConfigType +) -> cg.SafeExpType: + """Register a BLE client that only needs raw advertisement data. + + This does NOT register the ESP_BT_DEVICE feature, meaning ESPBTDevice + will not be compiled in if this is the only registration method used. + """ paren = await cg.get_variable(config[CONF_ESP32_BLE_ID]) cg.add(paren.register_client(var)) return var diff --git a/esphome/components/esp32_ble_tracker/automation.h b/esphome/components/esp32_ble_tracker/automation.h index 6bef9edcb3..ef677922e3 100644 --- a/esphome/components/esp32_ble_tracker/automation.h +++ b/esphome/components/esp32_ble_tracker/automation.h @@ -7,6 +7,7 @@ namespace esphome { namespace esp32_ble_tracker { +#ifdef USE_ESP32_BLE_DEVICE class ESPBTAdvertiseTrigger : public Trigger, public ESPBTDeviceListener { public: explicit ESPBTAdvertiseTrigger(ESP32BLETracker *parent) { parent->register_listener(this); } @@ -87,6 +88,7 @@ class BLEEndOfScanTrigger : public Trigger<>, public ESPBTDeviceListener { bool parse_device(const ESPBTDevice &device) override { return false; } void on_scan_end() override { this->trigger(); } }; +#endif // USE_ESP32_BLE_DEVICE template class ESP32BLEStartScanAction : public Action { public: diff --git a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp index d950ccb5f1..44577afbbd 100644 --- a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp +++ b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp @@ -141,6 +141,7 @@ void ESP32BLETracker::loop() { } if (this->parse_advertisements_) { +#ifdef USE_ESP32_BLE_DEVICE ESPBTDevice device; device.parse_scan_rst(scan_result); @@ -162,6 +163,7 @@ void ESP32BLETracker::loop() { if (!found && !this->scan_continuous_) { this->print_bt_device_info(device); } +#endif // USE_ESP32_BLE_DEVICE } // Move to next entry in ring buffer @@ -511,6 +513,7 @@ void ESP32BLETracker::set_scanner_state_(ScannerState state) { this->scanner_state_callbacks_.call(state); } +#ifdef USE_ESP32_BLE_DEVICE ESPBLEiBeacon::ESPBLEiBeacon(const uint8_t *data) { memcpy(&this->beacon_data_, data, sizeof(beacon_data_)); } optional ESPBLEiBeacon::from_manufacturer_data(const ServiceData &data) { if (!data.uuid.contains(0x4C, 0x00)) @@ -751,13 +754,16 @@ void ESPBTDevice::parse_adv_(const uint8_t *payload, uint8_t len) { } } } + std::string ESPBTDevice::address_str() const { char mac[24]; snprintf(mac, sizeof(mac), "%02X:%02X:%02X:%02X:%02X:%02X", this->address_[0], this->address_[1], this->address_[2], this->address_[3], this->address_[4], this->address_[5]); return mac; } + uint64_t ESPBTDevice::address_uint64() const { return esp32_ble::ble_addr_to_uint64(this->address_); } +#endif // USE_ESP32_BLE_DEVICE void ESP32BLETracker::dump_config() { ESP_LOGCONFIG(TAG, "BLE Tracker:"); @@ -796,6 +802,7 @@ void ESP32BLETracker::dump_config() { } } +#ifdef USE_ESP32_BLE_DEVICE void ESP32BLETracker::print_bt_device_info(const ESPBTDevice &device) { const uint64_t address = device.address_uint64(); for (auto &disc : this->already_discovered_) { @@ -866,8 +873,9 @@ bool ESPBTDevice::resolve_irk(const uint8_t *irk) const { return ecb_ciphertext[15] == (addr64 & 0xff) && ecb_ciphertext[14] == ((addr64 >> 8) & 0xff) && ecb_ciphertext[13] == ((addr64 >> 16) & 0xff); } +#endif // USE_ESP32_BLE_DEVICE } // namespace esp32_ble_tracker } // namespace esphome -#endif +#endif // USE_ESP32 diff --git a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.h b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.h index f5ed75a93e..e10f4551e8 100644 --- a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.h +++ b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.h @@ -39,6 +39,7 @@ struct ServiceData { adv_data_t data; }; +#ifdef USE_ESP32_BLE_DEVICE class ESPBLEiBeacon { public: ESPBLEiBeacon() { memset(&this->beacon_data_, 0, sizeof(this->beacon_data_)); } @@ -116,13 +117,16 @@ class ESPBTDevice { std::vector service_datas_{}; const BLEScanResult *scan_result_{nullptr}; }; +#endif // USE_ESP32_BLE_DEVICE class ESP32BLETracker; class ESPBTDeviceListener { public: virtual void on_scan_end() {} +#ifdef USE_ESP32_BLE_DEVICE virtual bool parse_device(const ESPBTDevice &device) = 0; +#endif virtual bool parse_devices(const BLEScanResult *scan_results, size_t count) { return false; }; virtual AdvertisementParserType get_advertisement_parser_type() { return AdvertisementParserType::PARSED_ADVERTISEMENTS; @@ -237,7 +241,9 @@ class ESP32BLETracker : public Component, void register_client(ESPBTClient *client); void recalculate_advertisement_parser_types(); +#ifdef USE_ESP32_BLE_DEVICE void print_bt_device_info(const ESPBTDevice &device); +#endif void start_scan(); void stop_scan(); diff --git a/esphome/core/defines.h b/esphome/core/defines.h index 8ed8f4b5aa..7ddb3436cd 100644 --- a/esphome/core/defines.h +++ b/esphome/core/defines.h @@ -145,6 +145,7 @@ #define USE_CAPTIVE_PORTAL #define USE_ESP32_BLE #define USE_ESP32_BLE_CLIENT +#define USE_ESP32_BLE_DEVICE #define USE_ESP32_BLE_SERVER #define USE_I2C #define USE_IMPROV From f745135bdc1e15e36192b22b84facc72d4553592 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 15 Jul 2025 15:20:58 -1000 Subject: [PATCH 07/14] Drop Python 3.10 support, require Python 3.11+ (#9522) Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com> --- .github/workflows/ci-docker.yml | 2 +- .github/workflows/ci.yml | 9 ++------ .github/workflows/release.yml | 2 +- .pre-commit-config.yaml | 2 +- esphome/components/lvgl/styles.py | 1 + esphome/dashboard/dns.py | 10 ++------- pyproject.toml | 6 ++--- requirements.txt | 1 - script/lint-python | 2 +- tests/integration/conftest.py | 6 ++--- .../test_api_message_size_batching.py | 2 +- tests/integration/test_api_reboot_timeout.py | 2 +- tests/integration/test_areas_and_devices.py | 2 +- tests/integration/test_device_id_in_state.py | 2 +- tests/integration/test_duplicate_entities.py | 2 +- tests/integration/test_entity_icon.py | 2 +- .../integration/test_host_mode_batch_delay.py | 2 +- .../test_host_mode_empty_string_options.py | 2 +- .../test_host_mode_entity_fields.py | 2 +- .../test_host_mode_many_entities.py | 2 +- ...mode_many_entities_multiple_connections.py | 2 +- tests/integration/test_host_mode_sensor.py | 2 +- tests/integration/test_loop_disable_enable.py | 20 ++++++++--------- .../test_scheduler_bulk_cleanup.py | 2 +- .../test_scheduler_defer_cancel.py | 2 +- .../test_scheduler_defer_cancel_regular.py | 2 +- .../test_scheduler_defer_fifo_simple.py | 4 ++-- .../test_scheduler_defer_stress.py | 2 +- .../integration/test_scheduler_heap_stress.py | 2 +- tests/integration/test_scheduler_null_name.py | 2 +- .../test_scheduler_rapid_cancellation.py | 2 +- .../test_scheduler_recursive_timeout.py | 2 +- .../test_scheduler_simultaneous_callbacks.py | 2 +- .../test_scheduler_string_lifetime.py | 2 +- .../test_scheduler_string_name_stress.py | 2 +- .../integration/test_scheduler_string_test.py | 22 +++++++++---------- 36 files changed, 61 insertions(+), 72 deletions(-) diff --git a/.github/workflows/ci-docker.yml b/.github/workflows/ci-docker.yml index f76ebba8e9..d6dac66359 100644 --- a/.github/workflows/ci-docker.yml +++ b/.github/workflows/ci-docker.yml @@ -47,7 +47,7 @@ jobs: - name: Set up Python uses: actions/setup-python@v5.6.0 with: - python-version: "3.10" + python-version: "3.11" - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3.11.1 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2f63a16844..b3f290c43f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -20,8 +20,8 @@ permissions: contents: read env: - DEFAULT_PYTHON: "3.10" - PYUPGRADE_TARGET: "--py310-plus" + DEFAULT_PYTHON: "3.11" + PYUPGRADE_TARGET: "--py311-plus" concurrency: # yamllint disable-line rule:line-length @@ -112,7 +112,6 @@ jobs: fail-fast: false matrix: python-version: - - "3.10" - "3.11" - "3.12" - "3.13" @@ -128,14 +127,10 @@ jobs: os: windows-latest - python-version: "3.12" os: windows-latest - - python-version: "3.10" - os: windows-latest - python-version: "3.13" os: macOS-latest - python-version: "3.12" os: macOS-latest - - python-version: "3.10" - os: macOS-latest runs-on: ${{ matrix.os }} needs: - common diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index b4518b27b5..44919a6270 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -96,7 +96,7 @@ jobs: - name: Set up Python uses: actions/setup-python@v5.6.0 with: - python-version: "3.10" + python-version: "3.11" - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3.11.1 diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 118253861d..1ff9167faf 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -40,7 +40,7 @@ repos: rev: v3.20.0 hooks: - id: pyupgrade - args: [--py310-plus] + args: [--py311-plus] - repo: https://github.com/adrienverge/yamllint.git rev: v1.37.1 hooks: diff --git a/esphome/components/lvgl/styles.py b/esphome/components/lvgl/styles.py index 426dd3f229..11d7bca5fa 100644 --- a/esphome/components/lvgl/styles.py +++ b/esphome/components/lvgl/styles.py @@ -76,6 +76,7 @@ async def theme_to_code(config): for w_name, style in theme.items(): # Work around Python 3.10 bug with nested async comprehensions # With Python 3.11 this could be simplified + # TODO: Now that we require Python 3.11+, this can be updated to use nested comprehensions styles = {} for part, states in collect_parts(style).items(): styles[part] = { diff --git a/esphome/dashboard/dns.py b/esphome/dashboard/dns.py index ea85d338bf..98134062f4 100644 --- a/esphome/dashboard/dns.py +++ b/esphome/dashboard/dns.py @@ -3,15 +3,9 @@ from __future__ import annotations import asyncio from contextlib import suppress from ipaddress import ip_address -import sys from icmplib import NameLookupError, async_resolve -if sys.version_info >= (3, 11): - from asyncio import timeout as async_timeout -else: - from async_timeout import timeout as async_timeout - RESOLVE_TIMEOUT = 3.0 @@ -20,9 +14,9 @@ async def _async_resolve_wrapper(hostname: str) -> list[str] | Exception: with suppress(ValueError): return [str(ip_address(hostname))] try: - async with async_timeout(RESOLVE_TIMEOUT): + async with asyncio.timeout(RESOLVE_TIMEOUT): return await async_resolve(hostname) - except (asyncio.TimeoutError, NameLookupError, UnicodeError) as ex: + except (TimeoutError, NameLookupError, UnicodeError) as ex: return ex diff --git a/pyproject.toml b/pyproject.toml index 97b0df9eff..25b7f3a24a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -20,7 +20,7 @@ classifiers = [ "Programming Language :: Python :: 3", "Topic :: Home Automation", ] -requires-python = ">=3.10.0" +requires-python = ">=3.11.0" dynamic = ["dependencies", "optional-dependencies", "version"] @@ -62,7 +62,7 @@ addopts = [ ] [tool.pylint.MAIN] -py-version = "3.10" +py-version = "3.11" ignore = [ "api_pb2.py", ] @@ -106,7 +106,7 @@ expected-line-ending-format = "LF" [tool.ruff] required-version = ">=0.5.0" -target-version = "py310" +target-version = "py311" exclude = ['generated'] [tool.ruff.lint] diff --git a/requirements.txt b/requirements.txt index 8829208f30..f547f47389 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,3 @@ -async_timeout==5.0.1; python_version <= "3.10" cryptography==45.0.1 voluptuous==0.15.2 PyYAML==6.0.2 diff --git a/script/lint-python b/script/lint-python index 2c25e4aee0..18281c711e 100755 --- a/script/lint-python +++ b/script/lint-python @@ -137,7 +137,7 @@ def main(): print() print("Running pyupgrade...") print() - PYUPGRADE_TARGET = "--py310-plus" + PYUPGRADE_TARGET = "--py311-plus" for files in filesets: cmd = ["pyupgrade", PYUPGRADE_TARGET] + files log = get_err(*cmd) diff --git a/tests/integration/conftest.py b/tests/integration/conftest.py index e3ba09de43..6e2f398f49 100644 --- a/tests/integration/conftest.py +++ b/tests/integration/conftest.py @@ -395,7 +395,7 @@ async def wait_and_connect_api_client( # Wait for connection with timeout try: await asyncio.wait_for(connected_future, timeout=timeout) - except asyncio.TimeoutError: + except TimeoutError: raise TimeoutError(f"Failed to connect to API after {timeout} seconds") yield client @@ -575,12 +575,12 @@ async def run_binary_and_wait_for_port( process.send_signal(signal.SIGINT) try: await asyncio.wait_for(process.wait(), timeout=SIGINT_TIMEOUT) - except asyncio.TimeoutError: + except TimeoutError: # If SIGINT didn't work, try SIGTERM process.terminate() try: await asyncio.wait_for(process.wait(), timeout=SIGTERM_TIMEOUT) - except asyncio.TimeoutError: + except TimeoutError: # Last resort: SIGKILL process.kill() await process.wait() diff --git a/tests/integration/test_api_message_size_batching.py b/tests/integration/test_api_message_size_batching.py index 631e64825e..f7859eb902 100644 --- a/tests/integration/test_api_message_size_batching.py +++ b/tests/integration/test_api_message_size_batching.py @@ -177,7 +177,7 @@ async def test_api_message_size_batching( # Wait for states with timeout try: await asyncio.wait_for(states_future, timeout=5.0) - except asyncio.TimeoutError: + except TimeoutError: missing_keys = expected_keys - received_keys pytest.fail( f"Did not receive states from all entities within 5 seconds. " diff --git a/tests/integration/test_api_reboot_timeout.py b/tests/integration/test_api_reboot_timeout.py index dd9f5fbd1e..9cada0a296 100644 --- a/tests/integration/test_api_reboot_timeout.py +++ b/tests/integration/test_api_reboot_timeout.py @@ -29,7 +29,7 @@ async def test_api_reboot_timeout( # (0.5s reboot timeout + some margin for processing) try: await asyncio.wait_for(reboot_future, timeout=2.0) - except asyncio.TimeoutError: + except TimeoutError: pytest.fail("Device did not reboot within expected timeout") # Test passes if we get here - reboot was detected diff --git a/tests/integration/test_areas_and_devices.py b/tests/integration/test_areas_and_devices.py index 4184255724..55c96d896d 100644 --- a/tests/integration/test_areas_and_devices.py +++ b/tests/integration/test_areas_and_devices.py @@ -98,7 +98,7 @@ async def test_areas_and_devices( # Wait for sensor states try: await asyncio.wait_for(states_future, timeout=10.0) - except asyncio.TimeoutError: + except TimeoutError: pytest.fail( f"Did not receive all sensor states within 10 seconds. " f"Received {len(states)} states" diff --git a/tests/integration/test_device_id_in_state.py b/tests/integration/test_device_id_in_state.py index eaa91ec92e..fb61569e59 100644 --- a/tests/integration/test_device_id_in_state.py +++ b/tests/integration/test_device_id_in_state.py @@ -77,7 +77,7 @@ async def test_device_id_in_state( # Wait for states try: await asyncio.wait_for(states_future, timeout=10.0) - except asyncio.TimeoutError: + except TimeoutError: pytest.fail( f"Did not receive all entity states within 10 seconds. " f"Received {len(states)} states, expected {len(entity_device_mapping)}" diff --git a/tests/integration/test_duplicate_entities.py b/tests/integration/test_duplicate_entities.py index b7ee8dd478..2c1fcba0eb 100644 --- a/tests/integration/test_duplicate_entities.py +++ b/tests/integration/test_duplicate_entities.py @@ -206,7 +206,7 @@ async def test_duplicate_entities_not_allowed_on_different_devices( # Wait for all entity states try: await asyncio.wait_for(states_future, timeout=10.0) - except asyncio.TimeoutError: + except TimeoutError: pytest.fail( f"Did not receive all entity states within 10 seconds. " f"Expected {expected_count}, received {state_count}" diff --git a/tests/integration/test_entity_icon.py b/tests/integration/test_entity_icon.py index aec7168165..a634ae385e 100644 --- a/tests/integration/test_entity_icon.py +++ b/tests/integration/test_entity_icon.py @@ -82,7 +82,7 @@ async def test_entity_icon( # Wait for states try: await asyncio.wait_for(state_received.wait(), timeout=5.0) - except asyncio.TimeoutError: + except TimeoutError: pytest.fail("No states received within 5 seconds") # Verify we received states diff --git a/tests/integration/test_host_mode_batch_delay.py b/tests/integration/test_host_mode_batch_delay.py index 5165b90e47..a3f666fa21 100644 --- a/tests/integration/test_host_mode_batch_delay.py +++ b/tests/integration/test_host_mode_batch_delay.py @@ -44,7 +44,7 @@ async def test_host_mode_batch_delay( # Wait for states from all entities with timeout try: entity_count = await asyncio.wait_for(entity_count_future, timeout=5.0) - except asyncio.TimeoutError: + except TimeoutError: pytest.fail( f"Did not receive states from at least 7 entities within 5 seconds. " f"Received {len(states)} states" diff --git a/tests/integration/test_host_mode_empty_string_options.py b/tests/integration/test_host_mode_empty_string_options.py index 16399dcfb8..242db2d40f 100644 --- a/tests/integration/test_host_mode_empty_string_options.py +++ b/tests/integration/test_host_mode_empty_string_options.py @@ -99,7 +99,7 @@ async def test_host_mode_empty_string_options( # Wait for initial states with timeout try: await asyncio.wait_for(states_received_future, timeout=5.0) - except asyncio.TimeoutError: + except TimeoutError: pytest.fail( f"Did not receive states for all select entities. " f"Expected keys: {expected_select_keys}, Received: {received_select_keys}" diff --git a/tests/integration/test_host_mode_entity_fields.py b/tests/integration/test_host_mode_entity_fields.py index b9fa3e9746..5ec1b64a99 100644 --- a/tests/integration/test_host_mode_entity_fields.py +++ b/tests/integration/test_host_mode_entity_fields.py @@ -86,7 +86,7 @@ async def test_host_mode_entity_fields( # Wait for at least one state try: await asyncio.wait_for(state_received.wait(), timeout=5.0) - except asyncio.TimeoutError: + except TimeoutError: pytest.fail("No states received within 5 seconds") # Verify we received states (which means has_state flag is working) diff --git a/tests/integration/test_host_mode_many_entities.py b/tests/integration/test_host_mode_many_entities.py index 19d1ee315f..ce9e157a88 100644 --- a/tests/integration/test_host_mode_many_entities.py +++ b/tests/integration/test_host_mode_many_entities.py @@ -41,7 +41,7 @@ async def test_host_mode_many_entities( # Wait for states from at least 50 sensors with timeout try: sensor_count = await asyncio.wait_for(sensor_count_future, timeout=10.0) - except asyncio.TimeoutError: + except TimeoutError: sensor_states = [ s for s in states.values() diff --git a/tests/integration/test_host_mode_many_entities_multiple_connections.py b/tests/integration/test_host_mode_many_entities_multiple_connections.py index a4e5f8a45c..a7939bb277 100644 --- a/tests/integration/test_host_mode_many_entities_multiple_connections.py +++ b/tests/integration/test_host_mode_many_entities_multiple_connections.py @@ -50,7 +50,7 @@ async def test_host_mode_many_entities_multiple_connections( asyncio.wait_for(client1_ready, timeout=10.0), asyncio.wait_for(client2_ready, timeout=10.0), ) - except asyncio.TimeoutError: + except TimeoutError: pytest.fail( f"One or both clients did not receive enough states within 10 seconds. " f"Client1: {len(states1)}, Client2: {len(states2)}" diff --git a/tests/integration/test_host_mode_sensor.py b/tests/integration/test_host_mode_sensor.py index 8c1e9f5d51..e28d3419e6 100644 --- a/tests/integration/test_host_mode_sensor.py +++ b/tests/integration/test_host_mode_sensor.py @@ -40,7 +40,7 @@ async def test_host_mode_with_sensor( # Wait for sensor with specific value (42.0) with timeout try: test_sensor_state = await asyncio.wait_for(sensor_future, timeout=5.0) - except asyncio.TimeoutError: + except TimeoutError: pytest.fail( f"Sensor with value 42.0 not received within 5 seconds. " f"Received states: {list(states.values())}" diff --git a/tests/integration/test_loop_disable_enable.py b/tests/integration/test_loop_disable_enable.py index e93fc32178..2a866b1574 100644 --- a/tests/integration/test_loop_disable_enable.py +++ b/tests/integration/test_loop_disable_enable.py @@ -150,7 +150,7 @@ async def test_loop_disable_enable( # Wait for self_disable_10 to disable itself try: await asyncio.wait_for(self_disable_10_disabled.wait(), timeout=10.0) - except asyncio.TimeoutError: + except TimeoutError: pytest.fail("self_disable_10 did not disable itself within 10 seconds") # Verify it ran at least 10 times before disabling @@ -164,7 +164,7 @@ async def test_loop_disable_enable( # Wait for normal_component to run at least 10 times try: await asyncio.wait_for(normal_component_10_loops.wait(), timeout=10.0) - except asyncio.TimeoutError: + except TimeoutError: pytest.fail( f"normal_component did not reach 10 loops within timeout, got {len(normal_component_counts)}" ) @@ -172,12 +172,12 @@ async def test_loop_disable_enable( # Wait for redundant operation tests try: await asyncio.wait_for(redundant_enable_tested.wait(), timeout=10.0) - except asyncio.TimeoutError: + except TimeoutError: pytest.fail("redundant_enable did not test enabling when already enabled") try: await asyncio.wait_for(redundant_disable_tested.wait(), timeout=10.0) - except asyncio.TimeoutError: + except TimeoutError: pytest.fail( "redundant_disable did not test disabling when will be disabled" ) @@ -185,7 +185,7 @@ async def test_loop_disable_enable( # Wait to see if self_disable_10 gets re-enabled try: await asyncio.wait_for(self_disable_10_re_enabled.wait(), timeout=5.0) - except asyncio.TimeoutError: + except TimeoutError: pytest.fail("self_disable_10 was not re-enabled within 5 seconds") # Component was re-enabled - verify it ran more times @@ -198,7 +198,7 @@ async def test_loop_disable_enable( # Wait for ISR component to disable itself after 5 loops try: await asyncio.wait_for(isr_component_disabled.wait(), timeout=3.0) - except asyncio.TimeoutError: + except TimeoutError: pytest.fail("ISR component did not disable itself within 3 seconds") # Verify it ran exactly 5 times before disabling @@ -210,7 +210,7 @@ async def test_loop_disable_enable( # Wait for component to be re-enabled by periodic ISR simulation and run again try: await asyncio.wait_for(isr_component_re_enabled.wait(), timeout=2.0) - except asyncio.TimeoutError: + except TimeoutError: pytest.fail("ISR component was not re-enabled after ISR call") # Verify it's running again after ISR enable @@ -222,7 +222,7 @@ async def test_loop_disable_enable( # Wait for pure ISR enable (no main loop enable) to work try: await asyncio.wait_for(isr_component_pure_re_enabled.wait(), timeout=2.0) - except asyncio.TimeoutError: + except TimeoutError: pytest.fail("ISR component was not re-enabled by pure ISR call") # Verify it ran after pure ISR enable @@ -235,7 +235,7 @@ async def test_loop_disable_enable( # Wait for update component to disable its loop try: await asyncio.wait_for(update_component_loop_disabled.wait(), timeout=3.0) - except asyncio.TimeoutError: + except TimeoutError: pytest.fail("Update component did not disable its loop within 3 seconds") # Verify it ran exactly 3 loops before disabling @@ -248,7 +248,7 @@ async def test_loop_disable_enable( await asyncio.wait_for( update_component_manual_update_called.wait(), timeout=5.0 ) - except asyncio.TimeoutError: + except TimeoutError: pytest.fail("Manual component.update was not called within 5 seconds") # The key test: verify that manual component.update worked after loop was disabled diff --git a/tests/integration/test_scheduler_bulk_cleanup.py b/tests/integration/test_scheduler_bulk_cleanup.py index 08ff293b84..b52a4a3496 100644 --- a/tests/integration/test_scheduler_bulk_cleanup.py +++ b/tests/integration/test_scheduler_bulk_cleanup.py @@ -103,7 +103,7 @@ async def test_scheduler_bulk_cleanup( # Wait for test completion try: await asyncio.wait_for(test_complete_future, timeout=10.0) - except asyncio.TimeoutError: + except TimeoutError: pytest.fail("Bulk cleanup test timed out") # Verify bulk cleanup was triggered diff --git a/tests/integration/test_scheduler_defer_cancel.py b/tests/integration/test_scheduler_defer_cancel.py index 923cf946c4..7bce0eda54 100644 --- a/tests/integration/test_scheduler_defer_cancel.py +++ b/tests/integration/test_scheduler_defer_cancel.py @@ -85,7 +85,7 @@ async def test_scheduler_defer_cancel( try: await asyncio.wait_for(test_complete_future, timeout=10.0) executed_defer = await asyncio.wait_for(test_result_future, timeout=1.0) - except asyncio.TimeoutError: + except TimeoutError: pytest.fail("Test did not complete within timeout") # Verify that only defer 10 was executed diff --git a/tests/integration/test_scheduler_defer_cancel_regular.py b/tests/integration/test_scheduler_defer_cancel_regular.py index 57b7134feb..c93d814fbe 100644 --- a/tests/integration/test_scheduler_defer_cancel_regular.py +++ b/tests/integration/test_scheduler_defer_cancel_regular.py @@ -64,7 +64,7 @@ async def test_scheduler_defer_cancels_regular( # Wait for test completion try: await asyncio.wait_for(test_complete_future, timeout=5.0) - except asyncio.TimeoutError: + except TimeoutError: pytest.fail(f"Test timed out. Log messages: {log_messages}") # Verify results diff --git a/tests/integration/test_scheduler_defer_fifo_simple.py b/tests/integration/test_scheduler_defer_fifo_simple.py index eb4058fedd..3502302368 100644 --- a/tests/integration/test_scheduler_defer_fifo_simple.py +++ b/tests/integration/test_scheduler_defer_fifo_simple.py @@ -90,7 +90,7 @@ async def test_scheduler_defer_fifo_simple( try: await asyncio.wait_for(test_complete_future, timeout=5.0) test1_passed = await asyncio.wait_for(test_result_future, timeout=1.0) - except asyncio.TimeoutError: + except TimeoutError: pytest.fail("Test set_timeout(0) did not complete within 5 seconds") assert test1_passed is True, ( @@ -108,7 +108,7 @@ async def test_scheduler_defer_fifo_simple( try: await asyncio.wait_for(test_complete_future, timeout=5.0) test2_passed = await asyncio.wait_for(test_result_future, timeout=1.0) - except asyncio.TimeoutError: + except TimeoutError: pytest.fail("Test defer() did not complete within 5 seconds") # Verify the test passed diff --git a/tests/integration/test_scheduler_defer_stress.py b/tests/integration/test_scheduler_defer_stress.py index d546b7132f..6f4d997307 100644 --- a/tests/integration/test_scheduler_defer_stress.py +++ b/tests/integration/test_scheduler_defer_stress.py @@ -97,7 +97,7 @@ async def test_scheduler_defer_stress( # Wait for all defers to execute (should be quick) try: await asyncio.wait_for(test_complete_future, timeout=5.0) - except asyncio.TimeoutError: + except TimeoutError: # Report how many we got pytest.fail( f"Stress test timed out. Only {len(executed_defers)} of " diff --git a/tests/integration/test_scheduler_heap_stress.py b/tests/integration/test_scheduler_heap_stress.py index 3c757bfc9d..1d6e1ec31e 100644 --- a/tests/integration/test_scheduler_heap_stress.py +++ b/tests/integration/test_scheduler_heap_stress.py @@ -104,7 +104,7 @@ async def test_scheduler_heap_stress( # Wait for all callbacks to execute (should be quick, but give more time for scheduling) try: await asyncio.wait_for(test_complete_future, timeout=60.0) - except asyncio.TimeoutError: + except TimeoutError: # Report how many we got pytest.fail( f"Stress test timed out. Only {len(executed_callbacks)} of " diff --git a/tests/integration/test_scheduler_null_name.py b/tests/integration/test_scheduler_null_name.py index 41bcd8aed7..66e25d4a11 100644 --- a/tests/integration/test_scheduler_null_name.py +++ b/tests/integration/test_scheduler_null_name.py @@ -53,7 +53,7 @@ async def test_scheduler_null_name( # Wait for test completion try: await asyncio.wait_for(test_complete_future, timeout=10.0) - except asyncio.TimeoutError: + except TimeoutError: pytest.fail( "Test did not complete within timeout - likely crashed due to NULL name" ) diff --git a/tests/integration/test_scheduler_rapid_cancellation.py b/tests/integration/test_scheduler_rapid_cancellation.py index 90577f36f1..6b6277c752 100644 --- a/tests/integration/test_scheduler_rapid_cancellation.py +++ b/tests/integration/test_scheduler_rapid_cancellation.py @@ -112,7 +112,7 @@ async def test_scheduler_rapid_cancellation( # Wait for test to complete with timeout try: await asyncio.wait_for(test_complete_future, timeout=10.0) - except asyncio.TimeoutError: + except TimeoutError: pytest.fail(f"Test timed out. Stats: {test_stats}") # Check for any errors diff --git a/tests/integration/test_scheduler_recursive_timeout.py b/tests/integration/test_scheduler_recursive_timeout.py index c015978e15..d98d2ac5ee 100644 --- a/tests/integration/test_scheduler_recursive_timeout.py +++ b/tests/integration/test_scheduler_recursive_timeout.py @@ -84,7 +84,7 @@ async def test_scheduler_recursive_timeout( # Wait for test to complete try: await asyncio.wait_for(test_complete_future, timeout=10.0) - except asyncio.TimeoutError: + except TimeoutError: pytest.fail( f"Recursive timeout test timed out. Got sequence: {execution_sequence}" ) diff --git a/tests/integration/test_scheduler_simultaneous_callbacks.py b/tests/integration/test_scheduler_simultaneous_callbacks.py index f5120ce4ce..82fd0fc01e 100644 --- a/tests/integration/test_scheduler_simultaneous_callbacks.py +++ b/tests/integration/test_scheduler_simultaneous_callbacks.py @@ -103,7 +103,7 @@ async def test_scheduler_simultaneous_callbacks( # Wait for test to complete try: await asyncio.wait_for(test_complete_future, timeout=30.0) - except asyncio.TimeoutError: + except TimeoutError: pytest.fail(f"Simultaneous callbacks test timed out. Stats: {test_stats}") # Check for any errors diff --git a/tests/integration/test_scheduler_string_lifetime.py b/tests/integration/test_scheduler_string_lifetime.py index 4d77abd954..7ec5a54373 100644 --- a/tests/integration/test_scheduler_string_lifetime.py +++ b/tests/integration/test_scheduler_string_lifetime.py @@ -157,7 +157,7 @@ async def test_scheduler_string_lifetime( client.execute_service(test_services["final"], {}) await asyncio.wait_for(all_tests_complete.wait(), timeout=5.0) - except asyncio.TimeoutError: + except TimeoutError: pytest.fail(f"String lifetime test timed out. Stats: {test_stats}") # Check for any errors diff --git a/tests/integration/test_scheduler_string_name_stress.py b/tests/integration/test_scheduler_string_name_stress.py index 3045842223..4c52913e63 100644 --- a/tests/integration/test_scheduler_string_name_stress.py +++ b/tests/integration/test_scheduler_string_name_stress.py @@ -97,7 +97,7 @@ async def test_scheduler_string_name_stress( # Wait for test to complete or crash try: await asyncio.wait_for(test_complete_future, timeout=30.0) - except asyncio.TimeoutError: + except TimeoutError: pytest.fail( f"String name stress test timed out. Executed {len(executed_callbacks)} callbacks. " f"This might indicate a deadlock." diff --git a/tests/integration/test_scheduler_string_test.py b/tests/integration/test_scheduler_string_test.py index f3a36b2db7..783ed37c13 100644 --- a/tests/integration/test_scheduler_string_test.py +++ b/tests/integration/test_scheduler_string_test.py @@ -122,22 +122,22 @@ async def test_scheduler_string_test( # Wait for static string tests try: await asyncio.wait_for(static_timeout_1_fired.wait(), timeout=0.5) - except asyncio.TimeoutError: + except TimeoutError: pytest.fail("Static timeout 1 did not fire within 0.5 seconds") try: await asyncio.wait_for(static_timeout_2_fired.wait(), timeout=0.5) - except asyncio.TimeoutError: + except TimeoutError: pytest.fail("Static timeout 2 did not fire within 0.5 seconds") try: await asyncio.wait_for(static_interval_fired.wait(), timeout=1.0) - except asyncio.TimeoutError: + except TimeoutError: pytest.fail("Static interval did not fire within 1 second") try: await asyncio.wait_for(static_interval_cancelled.wait(), timeout=2.0) - except asyncio.TimeoutError: + except TimeoutError: pytest.fail("Static interval was not cancelled within 2 seconds") # Verify static interval ran at least 3 times @@ -153,41 +153,41 @@ async def test_scheduler_string_test( # Wait for static defer tests try: await asyncio.wait_for(static_defer_1_fired.wait(), timeout=0.5) - except asyncio.TimeoutError: + except TimeoutError: pytest.fail("Static defer 1 did not fire within 0.5 seconds") try: await asyncio.wait_for(static_defer_2_fired.wait(), timeout=0.5) - except asyncio.TimeoutError: + except TimeoutError: pytest.fail("Static defer 2 did not fire within 0.5 seconds") # Wait for dynamic string tests try: await asyncio.wait_for(dynamic_timeout_fired.wait(), timeout=1.0) - except asyncio.TimeoutError: + except TimeoutError: pytest.fail("Dynamic timeout did not fire within 1 second") try: await asyncio.wait_for(dynamic_interval_fired.wait(), timeout=1.5) - except asyncio.TimeoutError: + except TimeoutError: pytest.fail("Dynamic interval did not fire within 1.5 seconds") # Wait for dynamic defer test try: await asyncio.wait_for(dynamic_defer_fired.wait(), timeout=1.0) - except asyncio.TimeoutError: + except TimeoutError: pytest.fail("Dynamic defer did not fire within 1 second") # Wait for cancel test try: await asyncio.wait_for(cancel_test_done.wait(), timeout=1.0) - except asyncio.TimeoutError: + except TimeoutError: pytest.fail("Cancel test did not complete within 1 second") # Wait for final results try: await asyncio.wait_for(final_results_logged.wait(), timeout=4.0) - except asyncio.TimeoutError: + except TimeoutError: pytest.fail("Final results were not logged within 4 seconds") # Verify results From ab54a880c13e65da6131148a019a4bfd89863c35 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 15 Jul 2025 15:25:41 -1000 Subject: [PATCH 08/14] Optimize MedianFilter memory allocation by adding vector reserve (#9531) --- esphome/components/sensor/filter.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/esphome/components/sensor/filter.cpp b/esphome/components/sensor/filter.cpp index dd8635f0c0..2fd56b7c8f 100644 --- a/esphome/components/sensor/filter.cpp +++ b/esphome/components/sensor/filter.cpp @@ -50,6 +50,7 @@ optional MedianFilter::new_value(float value) { if (!this->queue_.empty()) { // Copy queue without NaN values std::vector median_queue; + median_queue.reserve(this->queue_.size()); for (auto v : this->queue_) { if (!std::isnan(v)) { median_queue.push_back(v); From b695f13f862c0f5d409bb5cc3bc749d03786fca1 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Tue, 15 Jul 2025 21:40:28 -0400 Subject: [PATCH 09/14] [i2c] Use new driver with IDF 5.4.2+ (#8483) --- esphome/components/i2c/i2c_bus_esp_idf.cpp | 193 +++++++++++++++++++-- esphome/components/i2c/i2c_bus_esp_idf.h | 11 +- 2 files changed, 189 insertions(+), 15 deletions(-) diff --git a/esphome/components/i2c/i2c_bus_esp_idf.cpp b/esphome/components/i2c/i2c_bus_esp_idf.cpp index e4643405ce..c57d537bdb 100644 --- a/esphome/components/i2c/i2c_bus_esp_idf.cpp +++ b/esphome/components/i2c/i2c_bus_esp_idf.cpp @@ -7,6 +7,7 @@ #include "esphome/core/hal.h" #include "esphome/core/helpers.h" #include "esphome/core/log.h" +#include #if ESP_IDF_VERSION < ESP_IDF_VERSION_VAL(5, 3, 0) #define SOC_HP_I2C_NUM SOC_I2C_NUM @@ -20,21 +21,72 @@ static const char *const TAG = "i2c.idf"; void IDFI2CBus::setup() { ESP_LOGCONFIG(TAG, "Running setup"); static i2c_port_t next_port = I2C_NUM_0; - port_ = next_port; + this->port_ = next_port; + if (this->port_ == I2C_NUM_MAX) { + ESP_LOGE(TAG, "No more than %u buses supported", I2C_NUM_MAX); + this->mark_failed(); + return; + } + + if (this->timeout_ > 13000) { + ESP_LOGW(TAG, "Using max allowed timeout: 13 ms"); + this->timeout_ = 13000; + } + + this->recover_(); + +#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 4, 2) + next_port = (i2c_port_t) (next_port + 1); + + i2c_master_bus_config_t bus_conf{}; + memset(&bus_conf, 0, sizeof(bus_conf)); + bus_conf.sda_io_num = gpio_num_t(sda_pin_); + bus_conf.scl_io_num = gpio_num_t(scl_pin_); + bus_conf.i2c_port = this->port_; + bus_conf.glitch_ignore_cnt = 7; +#if SOC_LP_I2C_SUPPORTED + if (this->port_ < SOC_HP_I2C_NUM) { + bus_conf.clk_source = I2C_CLK_SRC_DEFAULT; + } else { + bus_conf.lp_source_clk = LP_I2C_SCLK_DEFAULT; + } +#else + bus_conf.clk_source = I2C_CLK_SRC_DEFAULT; +#endif + bus_conf.flags.enable_internal_pullup = sda_pullup_enabled_ || scl_pullup_enabled_; + esp_err_t err = i2c_new_master_bus(&bus_conf, &this->bus_); + if (err != ESP_OK) { + ESP_LOGW(TAG, "i2c_new_master_bus failed: %s", esp_err_to_name(err)); + this->mark_failed(); + return; + } + + i2c_device_config_t dev_conf{}; + memset(&dev_conf, 0, sizeof(dev_conf)); + dev_conf.dev_addr_length = I2C_ADDR_BIT_LEN_7; + dev_conf.device_address = I2C_DEVICE_ADDRESS_NOT_USED; + dev_conf.scl_speed_hz = this->frequency_; + dev_conf.scl_wait_us = this->timeout_; + err = i2c_master_bus_add_device(this->bus_, &dev_conf, &this->dev_); + if (err != ESP_OK) { + ESP_LOGW(TAG, "i2c_master_bus_add_device failed: %s", esp_err_to_name(err)); + this->mark_failed(); + return; + } + + this->initialized_ = true; + + if (this->scan_) { + ESP_LOGV(TAG, "Scanning for devices"); + this->i2c_scan_(); + } +#else #if SOC_HP_I2C_NUM > 1 next_port = (next_port == I2C_NUM_0) ? I2C_NUM_1 : I2C_NUM_MAX; #else next_port = I2C_NUM_MAX; #endif - if (port_ == I2C_NUM_MAX) { - ESP_LOGE(TAG, "No more than %u buses supported", SOC_HP_I2C_NUM); - this->mark_failed(); - return; - } - - recover_(); - i2c_config_t conf{}; memset(&conf, 0, sizeof(conf)); conf.mode = I2C_MODE_MASTER; @@ -53,11 +105,7 @@ void IDFI2CBus::setup() { this->mark_failed(); return; } - if (timeout_ > 0) { // if timeout specified in yaml: - if (timeout_ > 13000) { - ESP_LOGW(TAG, "i2c timeout of %" PRIu32 "us greater than max of 13ms on esp-idf, setting to max", timeout_); - timeout_ = 13000; - } + if (timeout_ > 0) { err = i2c_set_timeout(port_, timeout_ * 80); // unit: APB 80MHz clock cycle if (err != ESP_OK) { ESP_LOGW(TAG, "i2c_set_timeout failed: %s", esp_err_to_name(err)); @@ -73,12 +121,15 @@ void IDFI2CBus::setup() { this->mark_failed(); return; } + initialized_ = true; if (this->scan_) { ESP_LOGV(TAG, "Scanning bus for active devices"); this->i2c_scan_(); } +#endif } + void IDFI2CBus::dump_config() { ESP_LOGCONFIG(TAG, "I2C Bus:"); ESP_LOGCONFIG(TAG, @@ -123,6 +174,74 @@ ErrorCode IDFI2CBus::readv(uint8_t address, ReadBuffer *buffers, size_t cnt) { ESP_LOGVV(TAG, "i2c bus not initialized!"); return ERROR_NOT_INITIALIZED; } + +#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 4, 2) + i2c_operation_job_t jobs[cnt + 4]; + uint8_t read = (address << 1) | I2C_MASTER_READ; + size_t last = 0, num = 0; + + jobs[num].command = I2C_MASTER_CMD_START; + num++; + + jobs[num].command = I2C_MASTER_CMD_WRITE; + jobs[num].write.ack_check = true; + jobs[num].write.data = &read; + jobs[num].write.total_bytes = 1; + num++; + + // find the last valid index + for (size_t i = 0; i < cnt; i++) { + const auto &buf = buffers[i]; + if (buf.len == 0) { + continue; + } + last = i; + } + + for (size_t i = 0; i < cnt; i++) { + const auto &buf = buffers[i]; + if (buf.len == 0) { + continue; + } + if (i == last) { + // the last byte read before stop should always be a nack, + // split the last read if len is larger than 1 + if (buf.len > 1) { + jobs[num].command = I2C_MASTER_CMD_READ; + jobs[num].read.ack_value = I2C_ACK_VAL; + jobs[num].read.data = (uint8_t *) buf.data; + jobs[num].read.total_bytes = buf.len - 1; + num++; + } + jobs[num].command = I2C_MASTER_CMD_READ; + jobs[num].read.ack_value = I2C_NACK_VAL; + jobs[num].read.data = (uint8_t *) buf.data + buf.len - 1; + jobs[num].read.total_bytes = 1; + num++; + } else { + jobs[num].command = I2C_MASTER_CMD_READ; + jobs[num].read.ack_value = I2C_ACK_VAL; + jobs[num].read.data = (uint8_t *) buf.data; + jobs[num].read.total_bytes = buf.len; + num++; + } + } + + jobs[num].command = I2C_MASTER_CMD_STOP; + num++; + + esp_err_t err = i2c_master_execute_defined_operations(this->dev_, jobs, num, 20); + if (err == ESP_ERR_INVALID_STATE) { + ESP_LOGVV(TAG, "RX from %02X failed: not acked", address); + return ERROR_NOT_ACKNOWLEDGED; + } else if (err == ESP_ERR_TIMEOUT) { + ESP_LOGVV(TAG, "RX from %02X failed: timeout", address); + return ERROR_TIMEOUT; + } else if (err != ESP_OK) { + ESP_LOGVV(TAG, "RX from %02X failed: %s", address, esp_err_to_name(err)); + return ERROR_UNKNOWN; + } +#else i2c_cmd_handle_t cmd = i2c_cmd_link_create(); esp_err_t err = i2c_master_start(cmd); if (err != ESP_OK) { @@ -168,6 +287,7 @@ ErrorCode IDFI2CBus::readv(uint8_t address, ReadBuffer *buffers, size_t cnt) { ESP_LOGVV(TAG, "RX from %02X failed: %s", address, esp_err_to_name(err)); return ERROR_UNKNOWN; } +#endif #ifdef ESPHOME_LOG_HAS_VERY_VERBOSE char debug_buf[4]; @@ -185,6 +305,7 @@ ErrorCode IDFI2CBus::readv(uint8_t address, ReadBuffer *buffers, size_t cnt) { return ERROR_OK; } + ErrorCode IDFI2CBus::writev(uint8_t address, WriteBuffer *buffers, size_t cnt, bool stop) { // logging is only enabled with vv level, if warnings are shown the caller // should log them @@ -207,6 +328,49 @@ ErrorCode IDFI2CBus::writev(uint8_t address, WriteBuffer *buffers, size_t cnt, b ESP_LOGVV(TAG, "0x%02X TX %s", address, debug_hex.c_str()); #endif +#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 4, 2) + i2c_operation_job_t jobs[cnt + 3]; + uint8_t write = (address << 1) | I2C_MASTER_WRITE; + size_t num = 0; + + jobs[num].command = I2C_MASTER_CMD_START; + num++; + + jobs[num].command = I2C_MASTER_CMD_WRITE; + jobs[num].write.ack_check = true; + jobs[num].write.data = &write; + jobs[num].write.total_bytes = 1; + num++; + + for (size_t i = 0; i < cnt; i++) { + const auto &buf = buffers[i]; + if (buf.len == 0) { + continue; + } + jobs[num].command = I2C_MASTER_CMD_WRITE; + jobs[num].write.ack_check = true; + jobs[num].write.data = (uint8_t *) buf.data; + jobs[num].write.total_bytes = buf.len; + num++; + } + + if (stop) { + jobs[num].command = I2C_MASTER_CMD_STOP; + num++; + } + + esp_err_t err = i2c_master_execute_defined_operations(this->dev_, jobs, num, 20); + if (err == ESP_ERR_INVALID_STATE) { + ESP_LOGVV(TAG, "TX to %02X failed: not acked", address); + return ERROR_NOT_ACKNOWLEDGED; + } else if (err == ESP_ERR_TIMEOUT) { + ESP_LOGVV(TAG, "TX to %02X failed: timeout", address); + return ERROR_TIMEOUT; + } else if (err != ESP_OK) { + ESP_LOGVV(TAG, "TX to %02X failed: %s", address, esp_err_to_name(err)); + return ERROR_UNKNOWN; + } +#else i2c_cmd_handle_t cmd = i2c_cmd_link_create(); esp_err_t err = i2c_master_start(cmd); if (err != ESP_OK) { @@ -252,6 +416,7 @@ ErrorCode IDFI2CBus::writev(uint8_t address, WriteBuffer *buffers, size_t cnt, b ESP_LOGVV(TAG, "TX to %02X failed: %s", address, esp_err_to_name(err)); return ERROR_UNKNOWN; } +#endif return ERROR_OK; } diff --git a/esphome/components/i2c/i2c_bus_esp_idf.h b/esphome/components/i2c/i2c_bus_esp_idf.h index ee29578944..8d325de6bc 100644 --- a/esphome/components/i2c/i2c_bus_esp_idf.h +++ b/esphome/components/i2c/i2c_bus_esp_idf.h @@ -2,9 +2,14 @@ #ifdef USE_ESP_IDF -#include #include "esphome/core/component.h" #include "i2c_bus.h" +#include "esp_idf_version.h" +#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 4, 2) +#include +#else +#include +#endif namespace esphome { namespace i2c { @@ -38,6 +43,10 @@ class IDFI2CBus : public InternalI2CBus, public Component { RecoveryCode recovery_result_; protected: +#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 4, 2) + i2c_master_dev_handle_t dev_; + i2c_master_bus_handle_t bus_; +#endif i2c_port_t port_; uint8_t sda_pin_; bool sda_pullup_enabled_; From b1c86fe30ed10f0e1af85195ebc5b2775a97bed8 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 15 Jul 2025 15:41:55 -1000 Subject: [PATCH 10/14] Optimize scheduler timing by reducing millis() calls (#9524) --- esphome/core/application.cpp | 8 +++--- esphome/core/scheduler.cpp | 50 +++++++++++++++++------------------- esphome/core/scheduler.h | 8 +++--- 3 files changed, 31 insertions(+), 35 deletions(-) diff --git a/esphome/core/application.cpp b/esphome/core/application.cpp index 123d6d01f4..748c8f2237 100644 --- a/esphome/core/application.cpp +++ b/esphome/core/application.cpp @@ -71,7 +71,7 @@ void Application::setup() { do { uint8_t new_app_state = STATUS_LED_WARNING; - this->scheduler.call(); + this->scheduler.call(millis()); this->feed_wdt(); for (uint32_t j = 0; j <= i; j++) { // Update loop_component_start_time_ right before calling each component @@ -97,11 +97,11 @@ void Application::setup() { void Application::loop() { uint8_t new_app_state = 0; - this->scheduler.call(); - // Get the initial loop time at the start uint32_t last_op_end_time = millis(); + this->scheduler.call(last_op_end_time); + // Feed WDT with time this->feed_wdt(last_op_end_time); @@ -160,7 +160,7 @@ void Application::loop() { this->yield_with_select_(0); } else { uint32_t delay_time = this->loop_interval_ - elapsed; - uint32_t next_schedule = this->scheduler.next_schedule_in().value_or(delay_time); + uint32_t next_schedule = this->scheduler.next_schedule_in(last_op_end_time).value_or(delay_time); // next_schedule is max 0.5*delay_time // otherwise interval=0 schedules result in constant looping with almost no sleep next_schedule = std::max(next_schedule, delay_time / 2); diff --git a/esphome/core/scheduler.cpp b/esphome/core/scheduler.cpp index c6893b128f..1c37a1617d 100644 --- a/esphome/core/scheduler.cpp +++ b/esphome/core/scheduler.cpp @@ -91,7 +91,7 @@ void HOT Scheduler::set_timer_common_(Component *component, SchedulerItem::Type } #endif - const auto now = this->millis_(); + const auto now = this->millis_64_(millis()); // Type-specific setup if (type == SchedulerItem::INTERVAL) { @@ -193,9 +193,7 @@ void HOT Scheduler::set_retry(Component *component, const std::string &name, uin name.c_str(), initial_wait_time, max_attempts, backoff_increase_factor); if (backoff_increase_factor < 0.0001) { - ESP_LOGE(TAG, - "set_retry(name='%s'): backoff_factor cannot be close to zero nor negative (%0.1f). Using 1.0 instead", - name.c_str(), backoff_increase_factor); + ESP_LOGE(TAG, "backoff_factor %0.1f too small, using 1.0: %s", backoff_increase_factor, name.c_str()); backoff_increase_factor = 1; } @@ -215,19 +213,19 @@ bool HOT Scheduler::cancel_retry(Component *component, const std::string &name) return this->cancel_timeout(component, "retry$" + name); } -optional HOT Scheduler::next_schedule_in() { +optional HOT Scheduler::next_schedule_in(uint32_t now) { // IMPORTANT: This method should only be called from the main thread (loop task). // It calls empty_() and accesses items_[0] without holding a lock, which is only // safe when called from the main thread. Other threads must not call this method. if (this->empty_()) return {}; auto &item = this->items_[0]; - const auto now = this->millis_(); - if (item->next_execution_ < now) + const auto now_64 = this->millis_64_(now); + if (item->next_execution_ < now_64) return 0; - return item->next_execution_ - now; + return item->next_execution_ - now_64; } -void HOT Scheduler::call() { +void HOT Scheduler::call(uint32_t now) { #if !defined(USE_ESP8266) && !defined(USE_RP2040) // Process defer queue first to guarantee FIFO execution order for deferred items. // Previously, defer() used the heap which gave undefined order for equal timestamps, @@ -256,22 +254,22 @@ void HOT Scheduler::call() { // Execute callback without holding lock to prevent deadlocks // if the callback tries to call defer() again if (!this->should_skip_item_(item.get())) { - this->execute_item_(item.get()); + this->execute_item_(item.get(), now); } } #endif - const auto now = this->millis_(); + const auto now_64 = this->millis_64_(now); this->process_to_add(); #ifdef ESPHOME_DEBUG_SCHEDULER static uint64_t last_print = 0; - if (now - last_print > 2000) { - last_print = now; + if (now_64 - last_print > 2000) { + last_print = now_64; std::vector> old_items; - ESP_LOGD(TAG, "Items: count=%zu, now=%" PRIu64 " (%u, %" PRIu32 ")", this->items_.size(), now, this->millis_major_, - this->last_millis_); + ESP_LOGD(TAG, "Items: count=%zu, now=%" PRIu64 " (%u, %" PRIu32 ")", this->items_.size(), now_64, + this->millis_major_, this->last_millis_); while (!this->empty_()) { std::unique_ptr item; { @@ -283,7 +281,7 @@ void HOT Scheduler::call() { const char *name = item->get_name(); ESP_LOGD(TAG, " %s '%s/%s' interval=%" PRIu32 " next_execution in %" PRIu64 "ms at %" PRIu64, item->get_type_str(), item->get_source(), name ? name : "(null)", item->interval, - item->next_execution_ - now, item->next_execution_); + item->next_execution_ - now_64, item->next_execution_); old_items.push_back(std::move(item)); } @@ -328,7 +326,7 @@ void HOT Scheduler::call() { { // Don't copy-by value yet auto &item = this->items_[0]; - if (item->next_execution_ > now) { + if (item->next_execution_ > now_64) { // Not reached timeout yet, done for this call break; } @@ -342,13 +340,13 @@ void HOT Scheduler::call() { const char *item_name = item->get_name(); ESP_LOGV(TAG, "Running %s '%s/%s' with interval=%" PRIu32 " next_execution=%" PRIu64 " (now=%" PRIu64 ")", item->get_type_str(), item->get_source(), item_name ? item_name : "(null)", item->interval, - item->next_execution_, now); + item->next_execution_, now_64); #endif // Warning: During callback(), a lot of stuff can happen, including: // - timeouts/intervals get added, potentially invalidating vector pointers // - timeouts/intervals get cancelled - this->execute_item_(item.get()); + this->execute_item_(item.get(), now); } { @@ -367,7 +365,7 @@ void HOT Scheduler::call() { } if (item->type == SchedulerItem::INTERVAL) { - item->next_execution_ = now + item->interval; + item->next_execution_ = now_64 + item->interval; // Add new item directly to to_add_ // since we have the lock held this->to_add_.push_back(std::move(item)); @@ -423,11 +421,9 @@ void HOT Scheduler::pop_raw_() { } // Helper to execute a scheduler item -void HOT Scheduler::execute_item_(SchedulerItem *item) { +void HOT Scheduler::execute_item_(SchedulerItem *item, uint32_t now) { App.set_current_component(item->component); - - uint32_t now_ms = millis(); - WarnIfComponentBlockingGuard guard{item->component, now_ms}; + WarnIfComponentBlockingGuard guard{item->component, now}; item->callback(); guard.finish(); } @@ -486,15 +482,15 @@ bool HOT Scheduler::cancel_item_locked_(Component *component, const char *name_c return total_cancelled > 0; } -uint64_t Scheduler::millis_() { - // Get the current 32-bit millis value - const uint32_t now = millis(); +uint64_t Scheduler::millis_64_(uint32_t now) { // Check for rollover by comparing with last value if (now < this->last_millis_) { // Detected rollover (happens every ~49.7 days) this->millis_major_++; +#ifdef ESPHOME_DEBUG_SCHEDULER ESP_LOGD(TAG, "Incrementing scheduler major at %" PRIu64 "ms", now + (static_cast(this->millis_major_) << 32)); +#endif } this->last_millis_ = now; // Combine major (high 32 bits) and now (low 32 bits) into 64-bit time diff --git a/esphome/core/scheduler.h b/esphome/core/scheduler.h index 39cee5a876..ea5ac2e5f3 100644 --- a/esphome/core/scheduler.h +++ b/esphome/core/scheduler.h @@ -52,9 +52,9 @@ class Scheduler { std::function func, float backoff_increase_factor = 1.0f); bool cancel_retry(Component *component, const std::string &name); - optional next_schedule_in(); + optional next_schedule_in(uint32_t now); - void call(); + void call(uint32_t now); void process_to_add(); @@ -137,7 +137,7 @@ class Scheduler { void set_timer_common_(Component *component, SchedulerItem::Type type, bool is_static_string, const void *name_ptr, uint32_t delay, std::function func); - uint64_t millis_(); + uint64_t millis_64_(uint32_t now); void cleanup_(); void pop_raw_(); @@ -175,7 +175,7 @@ class Scheduler { } // Helper to execute a scheduler item - void execute_item_(SchedulerItem *item); + void execute_item_(SchedulerItem *item, uint32_t now); // Helper to check if item should be skipped bool should_skip_item_(const SchedulerItem *item) const { From e152690867a387e7f628143aa13e921df087c333 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 15 Jul 2025 15:42:55 -1000 Subject: [PATCH 11/14] Optimize API component LOGCONFIG usage for flash memory savings (#9526) --- esphome/components/api/api_server.cpp | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/esphome/components/api/api_server.cpp b/esphome/components/api/api_server.cpp index 750143d7f1..7b634de003 100644 --- a/esphome/components/api/api_server.cpp +++ b/esphome/components/api/api_server.cpp @@ -31,7 +31,6 @@ APIServer::APIServer() { } void APIServer::setup() { - ESP_LOGCONFIG(TAG, "Running setup"); this->setup_controller(); #ifdef USE_API_NOISE @@ -205,16 +204,16 @@ void APIServer::loop() { void APIServer::dump_config() { ESP_LOGCONFIG(TAG, - "API Server:\n" + "Server:\n" " Address: %s:%u", network::get_use_address().c_str(), this->port_); #ifdef USE_API_NOISE - ESP_LOGCONFIG(TAG, " Using noise encryption: %s", YESNO(this->noise_ctx_->has_psk())); + ESP_LOGCONFIG(TAG, " Noise encryption: %s", YESNO(this->noise_ctx_->has_psk())); if (!this->noise_ctx_->has_psk()) { - ESP_LOGCONFIG(TAG, " Supports noise encryption: YES"); + ESP_LOGCONFIG(TAG, " Supports encryption: YES"); } #else - ESP_LOGCONFIG(TAG, " Using noise encryption: NO"); + ESP_LOGCONFIG(TAG, " Noise encryption: NO"); #endif } From 40935f7ae4410aebcbf1825e89b36e3bd130a22f Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 15 Jul 2025 15:43:55 -1000 Subject: [PATCH 12/14] Skip API log message calls for unsubscribed log levels (#9514) --- esphome/components/api/api_connection.cpp | 3 --- esphome/components/api/api_connection.h | 1 + esphome/components/api/api_server.cpp | 2 +- 3 files changed, 2 insertions(+), 4 deletions(-) diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index f935518dbc..e6f5ea9d80 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -1404,9 +1404,6 @@ void APIConnection::update_command(const UpdateCommandRequest &msg) { #endif bool APIConnection::try_send_log_message(int level, const char *tag, const char *line, size_t message_len) { - if (this->flags_.log_subscription < level) - return false; - // Pre-calculate message size to avoid reallocations uint32_t msg_size = 0; diff --git a/esphome/components/api/api_connection.h b/esphome/components/api/api_connection.h index 0a3cb7b4d4..d7bf9a3858 100644 --- a/esphome/components/api/api_connection.h +++ b/esphome/components/api/api_connection.h @@ -209,6 +209,7 @@ class APIConnection : public APIServerConnection { return static_cast(this->flags_.connection_state) == ConnectionState::CONNECTED || this->is_authenticated(); } + uint8_t get_log_subscription_level() const { return this->flags_.log_subscription; } void on_fatal_error() override; void on_unauthenticated_access() override; void on_no_setup_connection() override; diff --git a/esphome/components/api/api_server.cpp b/esphome/components/api/api_server.cpp index 7b634de003..d95cec2f23 100644 --- a/esphome/components/api/api_server.cpp +++ b/esphome/components/api/api_server.cpp @@ -104,7 +104,7 @@ void APIServer::setup() { return; } for (auto &c : this->clients_) { - if (!c->flags_.remove) + if (!c->flags_.remove && c->get_log_subscription_level() >= level) c->try_send_log_message(level, tag, message, message_len); } }); From b648944973372cdb49a0a0e69bdfd02d1ce1968a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 15 Jul 2025 15:46:12 -1000 Subject: [PATCH 13/14] Optimize API connection batch priority message handling to reduce flash usage (#9510) --- esphome/components/api/api_connection.cpp | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index e6f5ea9d80..dbe3c3fa98 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -180,7 +180,8 @@ void APIConnection::loop() { on_fatal_error(); ESP_LOGW(TAG, "%s is unresponsive; disconnecting", this->get_client_combined_info().c_str()); } - } else if (now - this->last_traffic_ > KEEPALIVE_TIMEOUT_MS) { + } else if (now - this->last_traffic_ > KEEPALIVE_TIMEOUT_MS && !this->flags_.remove) { + // Only send ping if we're not disconnecting ESP_LOGVV(TAG, "Sending keepalive PING"); this->flags_.sent_ping = this->send_message(PingRequest()); if (!this->flags_.sent_ping) { @@ -1665,8 +1666,15 @@ void APIConnection::DeferredBatch::add_item(EntityBase *entity, MessageCreator c void APIConnection::DeferredBatch::add_item_front(EntityBase *entity, MessageCreator creator, uint8_t message_type, uint8_t estimated_size) { - // Insert at front for high priority messages (no deduplication check) - items.insert(items.begin(), BatchItem(entity, std::move(creator), message_type, estimated_size)); + // Add high priority message and swap to front + // This avoids expensive vector::insert which shifts all elements + // Note: We only ever have one high-priority message at a time (ping OR disconnect) + // If we're disconnecting, pings are blocked, so this simple swap is sufficient + items.emplace_back(entity, std::move(creator), message_type, estimated_size); + if (items.size() > 1) { + // Swap the new high-priority item to the front + std::swap(items.front(), items.back()); + } } bool APIConnection::schedule_batch_() { From c691f01c7f7ec29c978d0937b811232170b6239e Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 15 Jul 2025 15:50:32 -1000 Subject: [PATCH 14/14] Reduce flash usage by replacing ProtoSize template with specialized methods (#9487) --- esphome/components/api/api_pb2.cpp | 166 ++++++++++++++-------------- esphome/components/api/proto.h | 38 ++++++- script/api_protobuf/api_protobuf.py | 38 ++----- 3 files changed, 132 insertions(+), 110 deletions(-) diff --git a/esphome/components/api/api_pb2.cpp b/esphome/components/api/api_pb2.cpp index 797a33bfbd..b6212d915e 100644 --- a/esphome/components/api/api_pb2.cpp +++ b/esphome/components/api/api_pb2.cpp @@ -184,7 +184,7 @@ void ListEntitiesBinarySensorResponse::encode(ProtoWriteBuffer buffer) const { } void ListEntitiesBinarySensorResponse::calculate_size(uint32_t &total_size) const { ProtoSize::add_string_field(total_size, 1, this->object_id); - ProtoSize::add_fixed_field<4>(total_size, 1, this->key != 0); + ProtoSize::add_fixed32_field(total_size, 1, this->key); ProtoSize::add_string_field(total_size, 1, this->name); ProtoSize::add_string_field(total_size, 1, this->unique_id); ProtoSize::add_string_field(total_size, 1, this->device_class); @@ -201,7 +201,7 @@ void BinarySensorStateResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_uint32(4, this->device_id); } void BinarySensorStateResponse::calculate_size(uint32_t &total_size) const { - ProtoSize::add_fixed_field<4>(total_size, 1, this->key != 0); + ProtoSize::add_fixed32_field(total_size, 1, this->key); ProtoSize::add_bool_field(total_size, 1, this->state); ProtoSize::add_bool_field(total_size, 1, this->missing_state); ProtoSize::add_uint32_field(total_size, 1, this->device_id); @@ -225,7 +225,7 @@ void ListEntitiesCoverResponse::encode(ProtoWriteBuffer buffer) const { } void ListEntitiesCoverResponse::calculate_size(uint32_t &total_size) const { ProtoSize::add_string_field(total_size, 1, this->object_id); - ProtoSize::add_fixed_field<4>(total_size, 1, this->key != 0); + ProtoSize::add_fixed32_field(total_size, 1, this->key); ProtoSize::add_string_field(total_size, 1, this->name); ProtoSize::add_string_field(total_size, 1, this->unique_id); ProtoSize::add_bool_field(total_size, 1, this->assumed_state); @@ -247,10 +247,10 @@ void CoverStateResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_uint32(6, this->device_id); } void CoverStateResponse::calculate_size(uint32_t &total_size) const { - ProtoSize::add_fixed_field<4>(total_size, 1, this->key != 0); + ProtoSize::add_fixed32_field(total_size, 1, this->key); ProtoSize::add_enum_field(total_size, 1, static_cast(this->legacy_state)); - ProtoSize::add_fixed_field<4>(total_size, 1, this->position != 0.0f); - ProtoSize::add_fixed_field<4>(total_size, 1, this->tilt != 0.0f); + ProtoSize::add_float_field(total_size, 1, this->position); + ProtoSize::add_float_field(total_size, 1, this->tilt); ProtoSize::add_enum_field(total_size, 1, static_cast(this->current_operation)); ProtoSize::add_uint32_field(total_size, 1, this->device_id); } @@ -316,7 +316,7 @@ void ListEntitiesFanResponse::encode(ProtoWriteBuffer buffer) const { } void ListEntitiesFanResponse::calculate_size(uint32_t &total_size) const { ProtoSize::add_string_field(total_size, 1, this->object_id); - ProtoSize::add_fixed_field<4>(total_size, 1, this->key != 0); + ProtoSize::add_fixed32_field(total_size, 1, this->key); ProtoSize::add_string_field(total_size, 1, this->name); ProtoSize::add_string_field(total_size, 1, this->unique_id); ProtoSize::add_bool_field(total_size, 1, this->supports_oscillation); @@ -344,7 +344,7 @@ void FanStateResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_uint32(8, this->device_id); } void FanStateResponse::calculate_size(uint32_t &total_size) const { - ProtoSize::add_fixed_field<4>(total_size, 1, this->key != 0); + ProtoSize::add_fixed32_field(total_size, 1, this->key); ProtoSize::add_bool_field(total_size, 1, this->state); ProtoSize::add_bool_field(total_size, 1, this->oscillating); ProtoSize::add_enum_field(total_size, 1, static_cast(this->speed)); @@ -442,7 +442,7 @@ void ListEntitiesLightResponse::encode(ProtoWriteBuffer buffer) const { } void ListEntitiesLightResponse::calculate_size(uint32_t &total_size) const { ProtoSize::add_string_field(total_size, 1, this->object_id); - ProtoSize::add_fixed_field<4>(total_size, 1, this->key != 0); + ProtoSize::add_fixed32_field(total_size, 1, this->key); ProtoSize::add_string_field(total_size, 1, this->name); ProtoSize::add_string_field(total_size, 1, this->unique_id); if (!this->supported_color_modes.empty()) { @@ -454,8 +454,8 @@ void ListEntitiesLightResponse::calculate_size(uint32_t &total_size) const { ProtoSize::add_bool_field(total_size, 1, this->legacy_supports_rgb); ProtoSize::add_bool_field(total_size, 1, this->legacy_supports_white_value); ProtoSize::add_bool_field(total_size, 1, this->legacy_supports_color_temperature); - ProtoSize::add_fixed_field<4>(total_size, 1, this->min_mireds != 0.0f); - ProtoSize::add_fixed_field<4>(total_size, 1, this->max_mireds != 0.0f); + ProtoSize::add_float_field(total_size, 1, this->min_mireds); + ProtoSize::add_float_field(total_size, 1, this->max_mireds); if (!this->effects.empty()) { for (const auto &it : this->effects) { ProtoSize::add_string_field_repeated(total_size, 1, it); @@ -483,18 +483,18 @@ void LightStateResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_uint32(14, this->device_id); } void LightStateResponse::calculate_size(uint32_t &total_size) const { - ProtoSize::add_fixed_field<4>(total_size, 1, this->key != 0); + ProtoSize::add_fixed32_field(total_size, 1, this->key); ProtoSize::add_bool_field(total_size, 1, this->state); - ProtoSize::add_fixed_field<4>(total_size, 1, this->brightness != 0.0f); + ProtoSize::add_float_field(total_size, 1, this->brightness); ProtoSize::add_enum_field(total_size, 1, static_cast(this->color_mode)); - ProtoSize::add_fixed_field<4>(total_size, 1, this->color_brightness != 0.0f); - ProtoSize::add_fixed_field<4>(total_size, 1, this->red != 0.0f); - ProtoSize::add_fixed_field<4>(total_size, 1, this->green != 0.0f); - ProtoSize::add_fixed_field<4>(total_size, 1, this->blue != 0.0f); - ProtoSize::add_fixed_field<4>(total_size, 1, this->white != 0.0f); - ProtoSize::add_fixed_field<4>(total_size, 1, this->color_temperature != 0.0f); - ProtoSize::add_fixed_field<4>(total_size, 1, this->cold_white != 0.0f); - ProtoSize::add_fixed_field<4>(total_size, 1, this->warm_white != 0.0f); + ProtoSize::add_float_field(total_size, 1, this->color_brightness); + ProtoSize::add_float_field(total_size, 1, this->red); + ProtoSize::add_float_field(total_size, 1, this->green); + ProtoSize::add_float_field(total_size, 1, this->blue); + ProtoSize::add_float_field(total_size, 1, this->white); + ProtoSize::add_float_field(total_size, 1, this->color_temperature); + ProtoSize::add_float_field(total_size, 1, this->cold_white); + ProtoSize::add_float_field(total_size, 1, this->warm_white); ProtoSize::add_string_field(total_size, 1, this->effect); ProtoSize::add_uint32_field(total_size, 1, this->device_id); } @@ -623,7 +623,7 @@ void ListEntitiesSensorResponse::encode(ProtoWriteBuffer buffer) const { } void ListEntitiesSensorResponse::calculate_size(uint32_t &total_size) const { ProtoSize::add_string_field(total_size, 1, this->object_id); - ProtoSize::add_fixed_field<4>(total_size, 1, this->key != 0); + ProtoSize::add_fixed32_field(total_size, 1, this->key); ProtoSize::add_string_field(total_size, 1, this->name); ProtoSize::add_string_field(total_size, 1, this->unique_id); ProtoSize::add_string_field(total_size, 1, this->icon); @@ -644,8 +644,8 @@ void SensorStateResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_uint32(4, this->device_id); } void SensorStateResponse::calculate_size(uint32_t &total_size) const { - ProtoSize::add_fixed_field<4>(total_size, 1, this->key != 0); - ProtoSize::add_fixed_field<4>(total_size, 1, this->state != 0.0f); + ProtoSize::add_fixed32_field(total_size, 1, this->key); + ProtoSize::add_float_field(total_size, 1, this->state); ProtoSize::add_bool_field(total_size, 1, this->missing_state); ProtoSize::add_uint32_field(total_size, 1, this->device_id); } @@ -665,7 +665,7 @@ void ListEntitiesSwitchResponse::encode(ProtoWriteBuffer buffer) const { } void ListEntitiesSwitchResponse::calculate_size(uint32_t &total_size) const { ProtoSize::add_string_field(total_size, 1, this->object_id); - ProtoSize::add_fixed_field<4>(total_size, 1, this->key != 0); + ProtoSize::add_fixed32_field(total_size, 1, this->key); ProtoSize::add_string_field(total_size, 1, this->name); ProtoSize::add_string_field(total_size, 1, this->unique_id); ProtoSize::add_string_field(total_size, 1, this->icon); @@ -681,7 +681,7 @@ void SwitchStateResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_uint32(3, this->device_id); } void SwitchStateResponse::calculate_size(uint32_t &total_size) const { - ProtoSize::add_fixed_field<4>(total_size, 1, this->key != 0); + ProtoSize::add_fixed32_field(total_size, 1, this->key); ProtoSize::add_bool_field(total_size, 1, this->state); ProtoSize::add_uint32_field(total_size, 1, this->device_id); } @@ -723,7 +723,7 @@ void ListEntitiesTextSensorResponse::encode(ProtoWriteBuffer buffer) const { } void ListEntitiesTextSensorResponse::calculate_size(uint32_t &total_size) const { ProtoSize::add_string_field(total_size, 1, this->object_id); - ProtoSize::add_fixed_field<4>(total_size, 1, this->key != 0); + ProtoSize::add_fixed32_field(total_size, 1, this->key); ProtoSize::add_string_field(total_size, 1, this->name); ProtoSize::add_string_field(total_size, 1, this->unique_id); ProtoSize::add_string_field(total_size, 1, this->icon); @@ -739,7 +739,7 @@ void TextSensorStateResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_uint32(4, this->device_id); } void TextSensorStateResponse::calculate_size(uint32_t &total_size) const { - ProtoSize::add_fixed_field<4>(total_size, 1, this->key != 0); + ProtoSize::add_fixed32_field(total_size, 1, this->key); ProtoSize::add_string_field(total_size, 1, this->state); ProtoSize::add_bool_field(total_size, 1, this->missing_state); ProtoSize::add_uint32_field(total_size, 1, this->device_id); @@ -863,7 +863,7 @@ bool GetTimeResponse::decode_32bit(uint32_t field_id, Proto32Bit value) { } void GetTimeResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_fixed32(1, this->epoch_seconds); } void GetTimeResponse::calculate_size(uint32_t &total_size) const { - ProtoSize::add_fixed_field<4>(total_size, 1, this->epoch_seconds != 0); + ProtoSize::add_fixed32_field(total_size, 1, this->epoch_seconds); } #ifdef USE_API_SERVICES bool ListEntitiesServicesArgument::decode_varint(uint32_t field_id, ProtoVarInt value) { @@ -903,7 +903,7 @@ void ListEntitiesServicesResponse::encode(ProtoWriteBuffer buffer) const { } void ListEntitiesServicesResponse::calculate_size(uint32_t &total_size) const { ProtoSize::add_string_field(total_size, 1, this->name); - ProtoSize::add_fixed_field<4>(total_size, 1, this->key != 0); + ProtoSize::add_fixed32_field(total_size, 1, this->key); ProtoSize::add_repeated_message(total_size, 1, this->args); } bool ExecuteServiceArgument::decode_varint(uint32_t field_id, ProtoVarInt value) { @@ -976,7 +976,7 @@ void ExecuteServiceArgument::encode(ProtoWriteBuffer buffer) const { void ExecuteServiceArgument::calculate_size(uint32_t &total_size) const { ProtoSize::add_bool_field(total_size, 1, this->bool_); ProtoSize::add_int32_field(total_size, 1, this->legacy_int); - ProtoSize::add_fixed_field<4>(total_size, 1, this->float_ != 0.0f); + ProtoSize::add_float_field(total_size, 1, this->float_); ProtoSize::add_string_field(total_size, 1, this->string_); ProtoSize::add_sint32_field(total_size, 1, this->int_); if (!this->bool_array.empty()) { @@ -1033,7 +1033,7 @@ void ListEntitiesCameraResponse::encode(ProtoWriteBuffer buffer) const { } void ListEntitiesCameraResponse::calculate_size(uint32_t &total_size) const { ProtoSize::add_string_field(total_size, 1, this->object_id); - ProtoSize::add_fixed_field<4>(total_size, 1, this->key != 0); + ProtoSize::add_fixed32_field(total_size, 1, this->key); ProtoSize::add_string_field(total_size, 1, this->name); ProtoSize::add_string_field(total_size, 1, this->unique_id); ProtoSize::add_bool_field(total_size, 1, this->disabled_by_default); @@ -1048,7 +1048,7 @@ void CameraImageResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_uint32(4, this->device_id); } void CameraImageResponse::calculate_size(uint32_t &total_size) const { - ProtoSize::add_fixed_field<4>(total_size, 1, this->key != 0); + ProtoSize::add_fixed32_field(total_size, 1, this->key); ProtoSize::add_string_field(total_size, 1, this->data); ProtoSize::add_bool_field(total_size, 1, this->done); ProtoSize::add_uint32_field(total_size, 1, this->device_id); @@ -1110,7 +1110,7 @@ void ListEntitiesClimateResponse::encode(ProtoWriteBuffer buffer) const { } void ListEntitiesClimateResponse::calculate_size(uint32_t &total_size) const { ProtoSize::add_string_field(total_size, 1, this->object_id); - ProtoSize::add_fixed_field<4>(total_size, 1, this->key != 0); + ProtoSize::add_fixed32_field(total_size, 1, this->key); ProtoSize::add_string_field(total_size, 1, this->name); ProtoSize::add_string_field(total_size, 1, this->unique_id); ProtoSize::add_bool_field(total_size, 1, this->supports_current_temperature); @@ -1120,9 +1120,9 @@ void ListEntitiesClimateResponse::calculate_size(uint32_t &total_size) const { ProtoSize::add_enum_field_repeated(total_size, 1, static_cast(it)); } } - ProtoSize::add_fixed_field<4>(total_size, 1, this->visual_min_temperature != 0.0f); - ProtoSize::add_fixed_field<4>(total_size, 1, this->visual_max_temperature != 0.0f); - ProtoSize::add_fixed_field<4>(total_size, 1, this->visual_target_temperature_step != 0.0f); + ProtoSize::add_float_field(total_size, 1, this->visual_min_temperature); + ProtoSize::add_float_field(total_size, 1, this->visual_max_temperature); + ProtoSize::add_float_field(total_size, 1, this->visual_target_temperature_step); ProtoSize::add_bool_field(total_size, 1, this->legacy_supports_away); ProtoSize::add_bool_field(total_size, 1, this->supports_action); if (!this->supported_fan_modes.empty()) { @@ -1153,11 +1153,11 @@ void ListEntitiesClimateResponse::calculate_size(uint32_t &total_size) const { ProtoSize::add_bool_field(total_size, 2, this->disabled_by_default); ProtoSize::add_string_field(total_size, 2, this->icon); ProtoSize::add_enum_field(total_size, 2, static_cast(this->entity_category)); - ProtoSize::add_fixed_field<4>(total_size, 2, this->visual_current_temperature_step != 0.0f); + ProtoSize::add_float_field(total_size, 2, this->visual_current_temperature_step); ProtoSize::add_bool_field(total_size, 2, this->supports_current_humidity); ProtoSize::add_bool_field(total_size, 2, this->supports_target_humidity); - ProtoSize::add_fixed_field<4>(total_size, 2, this->visual_min_humidity != 0.0f); - ProtoSize::add_fixed_field<4>(total_size, 2, this->visual_max_humidity != 0.0f); + ProtoSize::add_float_field(total_size, 2, this->visual_min_humidity); + ProtoSize::add_float_field(total_size, 2, this->visual_max_humidity); ProtoSize::add_uint32_field(total_size, 2, this->device_id); } void ClimateStateResponse::encode(ProtoWriteBuffer buffer) const { @@ -1179,12 +1179,12 @@ void ClimateStateResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_uint32(16, this->device_id); } void ClimateStateResponse::calculate_size(uint32_t &total_size) const { - ProtoSize::add_fixed_field<4>(total_size, 1, this->key != 0); + ProtoSize::add_fixed32_field(total_size, 1, this->key); ProtoSize::add_enum_field(total_size, 1, static_cast(this->mode)); - ProtoSize::add_fixed_field<4>(total_size, 1, this->current_temperature != 0.0f); - ProtoSize::add_fixed_field<4>(total_size, 1, this->target_temperature != 0.0f); - ProtoSize::add_fixed_field<4>(total_size, 1, this->target_temperature_low != 0.0f); - ProtoSize::add_fixed_field<4>(total_size, 1, this->target_temperature_high != 0.0f); + ProtoSize::add_float_field(total_size, 1, this->current_temperature); + ProtoSize::add_float_field(total_size, 1, this->target_temperature); + ProtoSize::add_float_field(total_size, 1, this->target_temperature_low); + ProtoSize::add_float_field(total_size, 1, this->target_temperature_high); ProtoSize::add_bool_field(total_size, 1, this->unused_legacy_away); ProtoSize::add_enum_field(total_size, 1, static_cast(this->action)); ProtoSize::add_enum_field(total_size, 1, static_cast(this->fan_mode)); @@ -1192,8 +1192,8 @@ void ClimateStateResponse::calculate_size(uint32_t &total_size) const { ProtoSize::add_string_field(total_size, 1, this->custom_fan_mode); ProtoSize::add_enum_field(total_size, 1, static_cast(this->preset)); ProtoSize::add_string_field(total_size, 1, this->custom_preset); - ProtoSize::add_fixed_field<4>(total_size, 1, this->current_humidity != 0.0f); - ProtoSize::add_fixed_field<4>(total_size, 1, this->target_humidity != 0.0f); + ProtoSize::add_float_field(total_size, 1, this->current_humidity); + ProtoSize::add_float_field(total_size, 1, this->target_humidity); ProtoSize::add_uint32_field(total_size, 2, this->device_id); } bool ClimateCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { @@ -1309,13 +1309,13 @@ void ListEntitiesNumberResponse::encode(ProtoWriteBuffer buffer) const { } void ListEntitiesNumberResponse::calculate_size(uint32_t &total_size) const { ProtoSize::add_string_field(total_size, 1, this->object_id); - ProtoSize::add_fixed_field<4>(total_size, 1, this->key != 0); + ProtoSize::add_fixed32_field(total_size, 1, this->key); ProtoSize::add_string_field(total_size, 1, this->name); ProtoSize::add_string_field(total_size, 1, this->unique_id); ProtoSize::add_string_field(total_size, 1, this->icon); - ProtoSize::add_fixed_field<4>(total_size, 1, this->min_value != 0.0f); - ProtoSize::add_fixed_field<4>(total_size, 1, this->max_value != 0.0f); - ProtoSize::add_fixed_field<4>(total_size, 1, this->step != 0.0f); + ProtoSize::add_float_field(total_size, 1, this->min_value); + ProtoSize::add_float_field(total_size, 1, this->max_value); + ProtoSize::add_float_field(total_size, 1, this->step); ProtoSize::add_bool_field(total_size, 1, this->disabled_by_default); ProtoSize::add_enum_field(total_size, 1, static_cast(this->entity_category)); ProtoSize::add_string_field(total_size, 1, this->unit_of_measurement); @@ -1330,8 +1330,8 @@ void NumberStateResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_uint32(4, this->device_id); } void NumberStateResponse::calculate_size(uint32_t &total_size) const { - ProtoSize::add_fixed_field<4>(total_size, 1, this->key != 0); - ProtoSize::add_fixed_field<4>(total_size, 1, this->state != 0.0f); + ProtoSize::add_fixed32_field(total_size, 1, this->key); + ProtoSize::add_float_field(total_size, 1, this->state); ProtoSize::add_bool_field(total_size, 1, this->missing_state); ProtoSize::add_uint32_field(total_size, 1, this->device_id); } @@ -1375,7 +1375,7 @@ void ListEntitiesSelectResponse::encode(ProtoWriteBuffer buffer) const { } void ListEntitiesSelectResponse::calculate_size(uint32_t &total_size) const { ProtoSize::add_string_field(total_size, 1, this->object_id); - ProtoSize::add_fixed_field<4>(total_size, 1, this->key != 0); + ProtoSize::add_fixed32_field(total_size, 1, this->key); ProtoSize::add_string_field(total_size, 1, this->name); ProtoSize::add_string_field(total_size, 1, this->unique_id); ProtoSize::add_string_field(total_size, 1, this->icon); @@ -1395,7 +1395,7 @@ void SelectStateResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_uint32(4, this->device_id); } void SelectStateResponse::calculate_size(uint32_t &total_size) const { - ProtoSize::add_fixed_field<4>(total_size, 1, this->key != 0); + ProtoSize::add_fixed32_field(total_size, 1, this->key); ProtoSize::add_string_field(total_size, 1, this->state); ProtoSize::add_bool_field(total_size, 1, this->missing_state); ProtoSize::add_uint32_field(total_size, 1, this->device_id); @@ -1449,7 +1449,7 @@ void ListEntitiesSirenResponse::encode(ProtoWriteBuffer buffer) const { } void ListEntitiesSirenResponse::calculate_size(uint32_t &total_size) const { ProtoSize::add_string_field(total_size, 1, this->object_id); - ProtoSize::add_fixed_field<4>(total_size, 1, this->key != 0); + ProtoSize::add_fixed32_field(total_size, 1, this->key); ProtoSize::add_string_field(total_size, 1, this->name); ProtoSize::add_string_field(total_size, 1, this->unique_id); ProtoSize::add_string_field(total_size, 1, this->icon); @@ -1470,7 +1470,7 @@ void SirenStateResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_uint32(3, this->device_id); } void SirenStateResponse::calculate_size(uint32_t &total_size) const { - ProtoSize::add_fixed_field<4>(total_size, 1, this->key != 0); + ProtoSize::add_fixed32_field(total_size, 1, this->key); ProtoSize::add_bool_field(total_size, 1, this->state); ProtoSize::add_uint32_field(total_size, 1, this->device_id); } @@ -1543,7 +1543,7 @@ void ListEntitiesLockResponse::encode(ProtoWriteBuffer buffer) const { } void ListEntitiesLockResponse::calculate_size(uint32_t &total_size) const { ProtoSize::add_string_field(total_size, 1, this->object_id); - ProtoSize::add_fixed_field<4>(total_size, 1, this->key != 0); + ProtoSize::add_fixed32_field(total_size, 1, this->key); ProtoSize::add_string_field(total_size, 1, this->name); ProtoSize::add_string_field(total_size, 1, this->unique_id); ProtoSize::add_string_field(total_size, 1, this->icon); @@ -1561,7 +1561,7 @@ void LockStateResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_uint32(3, this->device_id); } void LockStateResponse::calculate_size(uint32_t &total_size) const { - ProtoSize::add_fixed_field<4>(total_size, 1, this->key != 0); + ProtoSize::add_fixed32_field(total_size, 1, this->key); ProtoSize::add_enum_field(total_size, 1, static_cast(this->state)); ProtoSize::add_uint32_field(total_size, 1, this->device_id); } @@ -1616,7 +1616,7 @@ void ListEntitiesButtonResponse::encode(ProtoWriteBuffer buffer) const { } void ListEntitiesButtonResponse::calculate_size(uint32_t &total_size) const { ProtoSize::add_string_field(total_size, 1, this->object_id); - ProtoSize::add_fixed_field<4>(total_size, 1, this->key != 0); + ProtoSize::add_fixed32_field(total_size, 1, this->key); ProtoSize::add_string_field(total_size, 1, this->name); ProtoSize::add_string_field(total_size, 1, this->unique_id); ProtoSize::add_string_field(total_size, 1, this->icon); @@ -1706,7 +1706,7 @@ void ListEntitiesMediaPlayerResponse::encode(ProtoWriteBuffer buffer) const { } void ListEntitiesMediaPlayerResponse::calculate_size(uint32_t &total_size) const { ProtoSize::add_string_field(total_size, 1, this->object_id); - ProtoSize::add_fixed_field<4>(total_size, 1, this->key != 0); + ProtoSize::add_fixed32_field(total_size, 1, this->key); ProtoSize::add_string_field(total_size, 1, this->name); ProtoSize::add_string_field(total_size, 1, this->unique_id); ProtoSize::add_string_field(total_size, 1, this->icon); @@ -1724,9 +1724,9 @@ void MediaPlayerStateResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_uint32(5, this->device_id); } void MediaPlayerStateResponse::calculate_size(uint32_t &total_size) const { - ProtoSize::add_fixed_field<4>(total_size, 1, this->key != 0); + ProtoSize::add_fixed32_field(total_size, 1, this->key); ProtoSize::add_enum_field(total_size, 1, static_cast(this->state)); - ProtoSize::add_fixed_field<4>(total_size, 1, this->volume != 0.0f); + ProtoSize::add_float_field(total_size, 1, this->volume); ProtoSize::add_bool_field(total_size, 1, this->muted); ProtoSize::add_uint32_field(total_size, 1, this->device_id); } @@ -2326,7 +2326,7 @@ void VoiceAssistantAudioSettings::encode(ProtoWriteBuffer buffer) const { void VoiceAssistantAudioSettings::calculate_size(uint32_t &total_size) const { ProtoSize::add_uint32_field(total_size, 1, this->noise_suppression_level); ProtoSize::add_uint32_field(total_size, 1, this->auto_gain); - ProtoSize::add_fixed_field<4>(total_size, 1, this->volume_multiplier != 0.0f); + ProtoSize::add_float_field(total_size, 1, this->volume_multiplier); } void VoiceAssistantRequest::encode(ProtoWriteBuffer buffer) const { buffer.encode_bool(1, this->start); @@ -2564,7 +2564,7 @@ void ListEntitiesAlarmControlPanelResponse::encode(ProtoWriteBuffer buffer) cons } void ListEntitiesAlarmControlPanelResponse::calculate_size(uint32_t &total_size) const { ProtoSize::add_string_field(total_size, 1, this->object_id); - ProtoSize::add_fixed_field<4>(total_size, 1, this->key != 0); + ProtoSize::add_fixed32_field(total_size, 1, this->key); ProtoSize::add_string_field(total_size, 1, this->name); ProtoSize::add_string_field(total_size, 1, this->unique_id); ProtoSize::add_string_field(total_size, 1, this->icon); @@ -2581,7 +2581,7 @@ void AlarmControlPanelStateResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_uint32(3, this->device_id); } void AlarmControlPanelStateResponse::calculate_size(uint32_t &total_size) const { - ProtoSize::add_fixed_field<4>(total_size, 1, this->key != 0); + ProtoSize::add_fixed32_field(total_size, 1, this->key); ProtoSize::add_enum_field(total_size, 1, static_cast(this->state)); ProtoSize::add_uint32_field(total_size, 1, this->device_id); } @@ -2636,7 +2636,7 @@ void ListEntitiesTextResponse::encode(ProtoWriteBuffer buffer) const { } void ListEntitiesTextResponse::calculate_size(uint32_t &total_size) const { ProtoSize::add_string_field(total_size, 1, this->object_id); - ProtoSize::add_fixed_field<4>(total_size, 1, this->key != 0); + ProtoSize::add_fixed32_field(total_size, 1, this->key); ProtoSize::add_string_field(total_size, 1, this->name); ProtoSize::add_string_field(total_size, 1, this->unique_id); ProtoSize::add_string_field(total_size, 1, this->icon); @@ -2655,7 +2655,7 @@ void TextStateResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_uint32(4, this->device_id); } void TextStateResponse::calculate_size(uint32_t &total_size) const { - ProtoSize::add_fixed_field<4>(total_size, 1, this->key != 0); + ProtoSize::add_fixed32_field(total_size, 1, this->key); ProtoSize::add_string_field(total_size, 1, this->state); ProtoSize::add_bool_field(total_size, 1, this->missing_state); ProtoSize::add_uint32_field(total_size, 1, this->device_id); @@ -2704,7 +2704,7 @@ void ListEntitiesDateResponse::encode(ProtoWriteBuffer buffer) const { } void ListEntitiesDateResponse::calculate_size(uint32_t &total_size) const { ProtoSize::add_string_field(total_size, 1, this->object_id); - ProtoSize::add_fixed_field<4>(total_size, 1, this->key != 0); + ProtoSize::add_fixed32_field(total_size, 1, this->key); ProtoSize::add_string_field(total_size, 1, this->name); ProtoSize::add_string_field(total_size, 1, this->unique_id); ProtoSize::add_string_field(total_size, 1, this->icon); @@ -2721,7 +2721,7 @@ void DateStateResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_uint32(6, this->device_id); } void DateStateResponse::calculate_size(uint32_t &total_size) const { - ProtoSize::add_fixed_field<4>(total_size, 1, this->key != 0); + ProtoSize::add_fixed32_field(total_size, 1, this->key); ProtoSize::add_bool_field(total_size, 1, this->missing_state); ProtoSize::add_uint32_field(total_size, 1, this->year); ProtoSize::add_uint32_field(total_size, 1, this->month); @@ -2771,7 +2771,7 @@ void ListEntitiesTimeResponse::encode(ProtoWriteBuffer buffer) const { } void ListEntitiesTimeResponse::calculate_size(uint32_t &total_size) const { ProtoSize::add_string_field(total_size, 1, this->object_id); - ProtoSize::add_fixed_field<4>(total_size, 1, this->key != 0); + ProtoSize::add_fixed32_field(total_size, 1, this->key); ProtoSize::add_string_field(total_size, 1, this->name); ProtoSize::add_string_field(total_size, 1, this->unique_id); ProtoSize::add_string_field(total_size, 1, this->icon); @@ -2788,7 +2788,7 @@ void TimeStateResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_uint32(6, this->device_id); } void TimeStateResponse::calculate_size(uint32_t &total_size) const { - ProtoSize::add_fixed_field<4>(total_size, 1, this->key != 0); + ProtoSize::add_fixed32_field(total_size, 1, this->key); ProtoSize::add_bool_field(total_size, 1, this->missing_state); ProtoSize::add_uint32_field(total_size, 1, this->hour); ProtoSize::add_uint32_field(total_size, 1, this->minute); @@ -2842,7 +2842,7 @@ void ListEntitiesEventResponse::encode(ProtoWriteBuffer buffer) const { } void ListEntitiesEventResponse::calculate_size(uint32_t &total_size) const { ProtoSize::add_string_field(total_size, 1, this->object_id); - ProtoSize::add_fixed_field<4>(total_size, 1, this->key != 0); + ProtoSize::add_fixed32_field(total_size, 1, this->key); ProtoSize::add_string_field(total_size, 1, this->name); ProtoSize::add_string_field(total_size, 1, this->unique_id); ProtoSize::add_string_field(total_size, 1, this->icon); @@ -2862,7 +2862,7 @@ void EventResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_uint32(3, this->device_id); } void EventResponse::calculate_size(uint32_t &total_size) const { - ProtoSize::add_fixed_field<4>(total_size, 1, this->key != 0); + ProtoSize::add_fixed32_field(total_size, 1, this->key); ProtoSize::add_string_field(total_size, 1, this->event_type); ProtoSize::add_uint32_field(total_size, 1, this->device_id); } @@ -2884,7 +2884,7 @@ void ListEntitiesValveResponse::encode(ProtoWriteBuffer buffer) const { } void ListEntitiesValveResponse::calculate_size(uint32_t &total_size) const { ProtoSize::add_string_field(total_size, 1, this->object_id); - ProtoSize::add_fixed_field<4>(total_size, 1, this->key != 0); + ProtoSize::add_fixed32_field(total_size, 1, this->key); ProtoSize::add_string_field(total_size, 1, this->name); ProtoSize::add_string_field(total_size, 1, this->unique_id); ProtoSize::add_string_field(total_size, 1, this->icon); @@ -2903,8 +2903,8 @@ void ValveStateResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_uint32(4, this->device_id); } void ValveStateResponse::calculate_size(uint32_t &total_size) const { - ProtoSize::add_fixed_field<4>(total_size, 1, this->key != 0); - ProtoSize::add_fixed_field<4>(total_size, 1, this->position != 0.0f); + ProtoSize::add_fixed32_field(total_size, 1, this->key); + ProtoSize::add_float_field(total_size, 1, this->position); ProtoSize::add_enum_field(total_size, 1, static_cast(this->current_operation)); ProtoSize::add_uint32_field(total_size, 1, this->device_id); } @@ -2951,7 +2951,7 @@ void ListEntitiesDateTimeResponse::encode(ProtoWriteBuffer buffer) const { } void ListEntitiesDateTimeResponse::calculate_size(uint32_t &total_size) const { ProtoSize::add_string_field(total_size, 1, this->object_id); - ProtoSize::add_fixed_field<4>(total_size, 1, this->key != 0); + ProtoSize::add_fixed32_field(total_size, 1, this->key); ProtoSize::add_string_field(total_size, 1, this->name); ProtoSize::add_string_field(total_size, 1, this->unique_id); ProtoSize::add_string_field(total_size, 1, this->icon); @@ -2966,9 +2966,9 @@ void DateTimeStateResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_uint32(4, this->device_id); } void DateTimeStateResponse::calculate_size(uint32_t &total_size) const { - ProtoSize::add_fixed_field<4>(total_size, 1, this->key != 0); + ProtoSize::add_fixed32_field(total_size, 1, this->key); ProtoSize::add_bool_field(total_size, 1, this->missing_state); - ProtoSize::add_fixed_field<4>(total_size, 1, this->epoch_seconds != 0); + ProtoSize::add_fixed32_field(total_size, 1, this->epoch_seconds); ProtoSize::add_uint32_field(total_size, 1, this->device_id); } bool DateTimeCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { @@ -3009,7 +3009,7 @@ void ListEntitiesUpdateResponse::encode(ProtoWriteBuffer buffer) const { } void ListEntitiesUpdateResponse::calculate_size(uint32_t &total_size) const { ProtoSize::add_string_field(total_size, 1, this->object_id); - ProtoSize::add_fixed_field<4>(total_size, 1, this->key != 0); + ProtoSize::add_fixed32_field(total_size, 1, this->key); ProtoSize::add_string_field(total_size, 1, this->name); ProtoSize::add_string_field(total_size, 1, this->unique_id); ProtoSize::add_string_field(total_size, 1, this->icon); @@ -3032,11 +3032,11 @@ void UpdateStateResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_uint32(11, this->device_id); } void UpdateStateResponse::calculate_size(uint32_t &total_size) const { - ProtoSize::add_fixed_field<4>(total_size, 1, this->key != 0); + ProtoSize::add_fixed32_field(total_size, 1, this->key); ProtoSize::add_bool_field(total_size, 1, this->missing_state); ProtoSize::add_bool_field(total_size, 1, this->in_progress); ProtoSize::add_bool_field(total_size, 1, this->has_progress); - ProtoSize::add_fixed_field<4>(total_size, 1, this->progress != 0.0f); + ProtoSize::add_float_field(total_size, 1, this->progress); ProtoSize::add_string_field(total_size, 1, this->current_version); ProtoSize::add_string_field(total_size, 1, this->latest_version); ProtoSize::add_string_field(total_size, 1, this->title); diff --git a/esphome/components/api/proto.h b/esphome/components/api/proto.h index f8539f4be1..83a03ba628 100644 --- a/esphome/components/api/proto.h +++ b/esphome/components/api/proto.h @@ -189,9 +189,9 @@ class ProtoWriteBuffer { * @param field_id Field number (tag) in the protobuf message * @param type Wire type value: * - 0: Varint (int32, int64, uint32, uint64, sint32, sint64, bool, enum) - * - 1: 64-bit (fixed64, sfixed64, double) * - 2: Length-delimited (string, bytes, embedded messages, packed repeated fields) * - 5: 32-bit (fixed32, sfixed32, float) + * - Note: Wire type 1 (64-bit fixed) is not supported * * Following https://protobuf.dev/programming-guides/encoding/#structure */ @@ -540,6 +540,42 @@ class ProtoSize { total_size += field_id_size + NumBytes; } + /** + * @brief Calculates and adds the size of a float field to the total message size + */ + static inline void add_float_field(uint32_t &total_size, uint32_t field_id_size, float value) { + if (value != 0.0f) { + total_size += field_id_size + 4; + } + } + + // NOTE: add_double_field removed - wire type 1 (64-bit: double) not supported + // to reduce overhead on embedded systems + + /** + * @brief Calculates and adds the size of a fixed32 field to the total message size + */ + static inline void add_fixed32_field(uint32_t &total_size, uint32_t field_id_size, uint32_t value) { + if (value != 0) { + total_size += field_id_size + 4; + } + } + + // NOTE: add_fixed64_field removed - wire type 1 (64-bit: fixed64) not supported + // to reduce overhead on embedded systems + + /** + * @brief Calculates and adds the size of a sfixed32 field to the total message size + */ + static inline void add_sfixed32_field(uint32_t &total_size, uint32_t field_id_size, int32_t value) { + if (value != 0) { + total_size += field_id_size + 4; + } + } + + // NOTE: add_sfixed64_field removed - wire type 1 (64-bit: sfixed64) not supported + // to reduce overhead on embedded systems + /** * @brief Calculates and adds the size of an enum field to the total message size * diff --git a/script/api_protobuf/api_protobuf.py b/script/api_protobuf/api_protobuf.py index f6e18d529d..fddddc7399 100755 --- a/script/api_protobuf/api_protobuf.py +++ b/script/api_protobuf/api_protobuf.py @@ -240,26 +240,6 @@ class TypeInfo(ABC): value = value_expr if value_expr else name return f"ProtoSize::{method}(total_size, {field_id_size}, {value});" - def _get_fixed_size_calculation( - self, name: str, force: bool, num_bytes: int, zero_check: str - ) -> str: - """Helper for fixed-size field calculations. - - Args: - name: Field name - force: Whether this is for a repeated field - num_bytes: Number of bytes (4 or 8) - zero_check: Expression to check for zero value (e.g., "!= 0.0f") - """ - field_id_size = self.calculate_field_id_size() - # Fixed-size repeated fields are handled differently in RepeatedTypeInfo - # so we should never get force=True here - assert not force, ( - "Fixed-size repeated fields should be handled by RepeatedTypeInfo" - ) - method = f"add_fixed_field<{num_bytes}>" - return f"ProtoSize::{method}(total_size, {field_id_size}, {name} {zero_check});" - @abstractmethod def get_size_calculation(self, name: str, force: bool = False) -> str: """Calculate the size needed for encoding this field. @@ -345,7 +325,8 @@ class DoubleType(TypeInfo): return o def get_size_calculation(self, name: str, force: bool = False) -> str: - return self._get_fixed_size_calculation(name, force, 8, "!= 0.0") + field_id_size = self.calculate_field_id_size() + return f"ProtoSize::add_double_field(total_size, {field_id_size}, {name});" def get_fixed_size_bytes(self) -> int: return 8 @@ -368,7 +349,8 @@ class FloatType(TypeInfo): return o def get_size_calculation(self, name: str, force: bool = False) -> str: - return self._get_fixed_size_calculation(name, force, 4, "!= 0.0f") + field_id_size = self.calculate_field_id_size() + return f"ProtoSize::add_float_field(total_size, {field_id_size}, {name});" def get_fixed_size_bytes(self) -> int: return 4 @@ -451,7 +433,8 @@ class Fixed64Type(TypeInfo): return o def get_size_calculation(self, name: str, force: bool = False) -> str: - return self._get_fixed_size_calculation(name, force, 8, "!= 0") + field_id_size = self.calculate_field_id_size() + return f"ProtoSize::add_fixed64_field(total_size, {field_id_size}, {name});" def get_fixed_size_bytes(self) -> int: return 8 @@ -474,7 +457,8 @@ class Fixed32Type(TypeInfo): return o def get_size_calculation(self, name: str, force: bool = False) -> str: - return self._get_fixed_size_calculation(name, force, 4, "!= 0") + field_id_size = self.calculate_field_id_size() + return f"ProtoSize::add_fixed32_field(total_size, {field_id_size}, {name});" def get_fixed_size_bytes(self) -> int: return 4 @@ -663,7 +647,8 @@ class SFixed32Type(TypeInfo): return o def get_size_calculation(self, name: str, force: bool = False) -> str: - return self._get_fixed_size_calculation(name, force, 4, "!= 0") + field_id_size = self.calculate_field_id_size() + return f"ProtoSize::add_sfixed32_field(total_size, {field_id_size}, {name});" def get_fixed_size_bytes(self) -> int: return 4 @@ -686,7 +671,8 @@ class SFixed64Type(TypeInfo): return o def get_size_calculation(self, name: str, force: bool = False) -> str: - return self._get_fixed_size_calculation(name, force, 8, "!= 0") + field_id_size = self.calculate_field_id_size() + return f"ProtoSize::add_sfixed64_field(total_size, {field_id_size}, {name});" def get_fixed_size_bytes(self) -> int: return 8