From de82f96ccb3b38480a441ae1aa255ada455f9c28 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 10 Jan 2026 14:43:31 -1000 Subject: [PATCH 1/6] [core] Rename FixedVector::shrink_to_fit() to release() for clarity (#13130) --- esphome/components/wifi/wifi_component.cpp | 4 ++-- esphome/core/helpers.h | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/esphome/components/wifi/wifi_component.cpp b/esphome/components/wifi/wifi_component.cpp index afdaa0b6e8..352081fe31 100644 --- a/esphome/components/wifi/wifi_component.cpp +++ b/esphome/components/wifi/wifi_component.cpp @@ -2014,8 +2014,8 @@ void WiFiComponent::release_scan_results_() { // std::vector - use swap trick since shrink_to_fit is non-binding decltype(this->scan_result_)().swap(this->scan_result_); #else - // FixedVector::shrink_to_fit() actually frees all memory - this->scan_result_.shrink_to_fit(); + // FixedVector::release() frees all memory + this->scan_result_.release(); #endif } } diff --git a/esphome/core/helpers.h b/esphome/core/helpers.h index acba420d3e..05d2d475c1 100644 --- a/esphome/core/helpers.h +++ b/esphome/core/helpers.h @@ -293,8 +293,8 @@ template class FixedVector { size_ = 0; } - // Shrink capacity to fit current size (frees all memory) - void shrink_to_fit() { + // Release all memory (destroys elements and frees memory) + void release() { cleanup_(); reset_(); } From 5725a4840e3900eaa7d8ca9cc3f9719161577197 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 11 Jan 2026 01:09:25 +0000 Subject: [PATCH 2/6] Bump aioesphomeapi from 43.10.1 to 43.11.0 (#13132) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 00f81f793f..eb177e1411 100644 --- a/requirements.txt +++ b/requirements.txt @@ -12,7 +12,7 @@ platformio==6.1.18 # When updating platformio, also update /docker/Dockerfile esptool==5.1.0 click==8.1.7 esphome-dashboard==20260110.0 -aioesphomeapi==43.10.1 +aioesphomeapi==43.11.0 zeroconf==0.148.0 puremagic==1.30 ruamel.yaml==0.19.1 # dashboard_import From f2eb61a767d986fa425efbc5ef233c3456054fab Mon Sep 17 00:00:00 2001 From: Keith Burzinski Date: Sat, 10 Jan 2026 19:43:27 -0600 Subject: [PATCH 3/6] [api] Proto code generator changes for #12985 (#13100) Co-authored-by: J. Nick Koston --- esphome/components/api/api_options.proto | 11 +++ script/api_protobuf/api_protobuf.py | 85 ++++++++++++++++++++++++ 2 files changed, 96 insertions(+) diff --git a/esphome/components/api/api_options.proto b/esphome/components/api/api_options.proto index 6b33408e2f..1916e84625 100644 --- a/esphome/components/api/api_options.proto +++ b/esphome/components/api/api_options.proto @@ -80,4 +80,15 @@ extend google.protobuf.FieldOptions { // Example: [(container_pointer_no_template) = "light::ColorModeMask"] // generates: const light::ColorModeMask *supported_color_modes{}; optional string container_pointer_no_template = 50014; + + // packed_buffer: Expose raw packed buffer instead of decoding into container + // When set on a packed repeated field, the generated code stores a pointer + // to the raw protobuf buffer instead of decoding values. This enables + // zero-copy passthrough when the consumer can decode on-demand. + // The field must be a packed repeated field (packed=true). + // Generates three fields: + // - const uint8_t *_data_{nullptr}; + // - uint16_t _length_{0}; + // - uint16_t _count_{0}; + optional bool packed_buffer = 50015 [default=false]; } diff --git a/script/api_protobuf/api_protobuf.py b/script/api_protobuf/api_protobuf.py index 274a672c7c..c61555805e 100755 --- a/script/api_protobuf/api_protobuf.py +++ b/script/api_protobuf/api_protobuf.py @@ -339,6 +339,9 @@ def create_field_type_info( ) -> TypeInfo: """Create the appropriate TypeInfo instance for a field, handling repeated fields and custom options.""" if field.label == FieldDescriptorProto.LABEL_REPEATED: + # Check if this is a packed_buffer field (zero-copy packed repeated) + if get_field_opt(field, pb.packed_buffer, False): + return PackedBufferTypeInfo(field) # Check if this repeated field has fixed_array_with_length_define option if ( fixed_size := get_field_opt(field, pb.fixed_array_with_length_define) @@ -947,6 +950,88 @@ class PointerToStringBufferType(PointerToBufferTypeBase): return self.calculate_field_id_size() + 8 # field ID + 8 bytes typical string +class PackedBufferTypeInfo(TypeInfo): + """Type for packed repeated fields that expose raw buffer instead of decoding. + + When a repeated field is marked with [(packed_buffer) = true], this type + generates code that stores a pointer to the raw protobuf buffer along with + its length and the count of values. This enables zero-copy passthrough when + the consumer can decode the packed varints on-demand. + """ + + def __init__(self, field: descriptor.FieldDescriptorProto) -> None: + # packed_buffer is decode-only (SOURCE_CLIENT messages) + super().__init__(field, needs_decode=True, needs_encode=False) + + @property + def cpp_type(self) -> str: + # Not used - we have multiple fields + return "const uint8_t*" + + @property + def wire_type(self) -> WireType: + """Packed fields use LENGTH_DELIMITED wire type.""" + return WireType.LENGTH_DELIMITED + + @property + def public_content(self) -> list[str]: + """Generate three fields: data pointer, length, and count.""" + return [ + f"const uint8_t *{self.field_name}_data_{{nullptr}};", + f"uint16_t {self.field_name}_length_{{0}};", + f"uint16_t {self.field_name}_count_{{0}};", + ] + + @property + def decode_length_content(self) -> str: + """Store pointer to buffer and calculate count of packed varints.""" + return f"""case {self.number}: {{ + this->{self.field_name}_data_ = value.data(); + this->{self.field_name}_length_ = value.size(); + this->{self.field_name}_count_ = count_packed_varints(value.data(), value.size()); + break; + }}""" + + @property + def encode_content(self) -> str: + """No encoding - this is decode-only for SOURCE_CLIENT messages.""" + return None + + @property + def dump_content(self) -> str: + """Dump shows buffer info but not decoded values.""" + return ( + f'out.append(" {self.name}: ");\n' + + 'out.append("packed buffer [");\n' + + f"out.append(std::to_string(this->{self.field_name}_count_));\n" + + 'out.append(" values, ");\n' + + f"out.append(std::to_string(this->{self.field_name}_length_));\n" + + 'out.append(" bytes]\\n");' + ) + + def dump(self, name: str) -> str: + """Dump method for packed buffer - not typically used but required by abstract base.""" + return 'out.append("packed buffer");' + + def get_size_calculation(self, name: str, force: bool = False) -> str: + """No size calculation needed - decode-only.""" + return "" + + def get_estimated_size(self) -> int: + """Estimate size for packed buffer field. + + Typical IR/RF timing array has ~50-200 values, each encoded as 1-3 bytes. + Estimate 100 values * 2 bytes = 200 bytes typical. + """ + return ( + self.calculate_field_id_size() + 2 + 200 + ) # field ID + length varint + data + + @classmethod + def can_use_dump_field(cls) -> bool: + return False + + class FixedArrayBytesType(TypeInfo): """Special type for fixed-size byte arrays.""" From e34532f2835adf621d22d3ae9b3c6023d71e46d5 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 10 Jan 2026 16:42:35 -1000 Subject: [PATCH 4/6] [sensor] Use C++17 nested namespace syntax (#13116) --- esphome/components/sensor/automation.cpp | 6 ++---- esphome/components/sensor/automation.h | 6 ++---- esphome/components/sensor/filter.cpp | 6 ++---- esphome/components/sensor/filter.h | 6 ++---- esphome/components/sensor/sensor.cpp | 6 ++---- esphome/components/sensor/sensor.h | 6 ++---- 6 files changed, 12 insertions(+), 24 deletions(-) diff --git a/esphome/components/sensor/automation.cpp b/esphome/components/sensor/automation.cpp index f53c43d1f6..977719db9b 100644 --- a/esphome/components/sensor/automation.cpp +++ b/esphome/components/sensor/automation.cpp @@ -1,10 +1,8 @@ #include "automation.h" #include "esphome/core/log.h" -namespace esphome { -namespace sensor { +namespace esphome::sensor { static const char *const TAG = "sensor.automation"; -} // namespace sensor -} // namespace esphome +} // namespace esphome::sensor diff --git a/esphome/components/sensor/automation.h b/esphome/components/sensor/automation.h index df7d31a0c9..996c7fc9b5 100644 --- a/esphome/components/sensor/automation.h +++ b/esphome/components/sensor/automation.h @@ -4,8 +4,7 @@ #include "esphome/core/automation.h" #include "esphome/components/sensor/sensor.h" -namespace esphome { -namespace sensor { +namespace esphome::sensor { class SensorStateTrigger : public Trigger { public: @@ -107,5 +106,4 @@ template class SensorInRangeCondition : public Condition float max_{NAN}; }; -} // namespace sensor -} // namespace esphome +} // namespace esphome::sensor diff --git a/esphome/components/sensor/filter.cpp b/esphome/components/sensor/filter.cpp index c8c6540112..8450ec4c4e 100644 --- a/esphome/components/sensor/filter.cpp +++ b/esphome/components/sensor/filter.cpp @@ -5,8 +5,7 @@ #include "esphome/core/log.h" #include "sensor.h" -namespace esphome { -namespace sensor { +namespace esphome::sensor { static const char *const TAG = "sensor.filter"; @@ -574,5 +573,4 @@ void StreamingMovingAverageFilter::reset_batch() { this->valid_count_ = 0; } -} // namespace sensor -} // namespace esphome +} // namespace esphome::sensor diff --git a/esphome/components/sensor/filter.h b/esphome/components/sensor/filter.h index 92a9184c18..15c7656a7b 100644 --- a/esphome/components/sensor/filter.h +++ b/esphome/components/sensor/filter.h @@ -7,8 +7,7 @@ #include "esphome/core/component.h" #include "esphome/core/helpers.h" -namespace esphome { -namespace sensor { +namespace esphome::sensor { class Sensor; @@ -632,5 +631,4 @@ class StreamingMovingAverageFilter : public StreamingFilter { size_t valid_count_{0}; }; -} // namespace sensor -} // namespace esphome +} // namespace esphome::sensor diff --git a/esphome/components/sensor/sensor.cpp b/esphome/components/sensor/sensor.cpp index c1d28bf260..64678f8d0c 100644 --- a/esphome/components/sensor/sensor.cpp +++ b/esphome/components/sensor/sensor.cpp @@ -3,8 +3,7 @@ #include "esphome/core/controller_registry.h" #include "esphome/core/log.h" -namespace esphome { -namespace sensor { +namespace esphome::sensor { static const char *const TAG = "sensor"; @@ -135,5 +134,4 @@ void Sensor::internal_send_state_to_frontend(float state) { #endif } -} // namespace sensor -} // namespace esphome +} // namespace esphome::sensor diff --git a/esphome/components/sensor/sensor.h b/esphome/components/sensor/sensor.h index a792c0d3fd..d9046020f6 100644 --- a/esphome/components/sensor/sensor.h +++ b/esphome/components/sensor/sensor.h @@ -9,8 +9,7 @@ #include #include -namespace esphome { -namespace sensor { +namespace esphome::sensor { void log_sensor(const char *tag, const char *prefix, const char *type, Sensor *obj); @@ -143,5 +142,4 @@ class Sensor : public EntityBase, public EntityBase_DeviceClass, public EntityBa } sensor_flags_{}; }; -} // namespace sensor -} // namespace esphome +} // namespace esphome::sensor From 6222fae907fbca9aca9f373081b74707cbc96e85 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 10 Jan 2026 16:43:15 -1000 Subject: [PATCH 5/6] [libretiny] Disable BLE stack on BK7231N to save ~21KB RAM (#13131) --- esphome/components/libretiny/__init__.py | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/esphome/components/libretiny/__init__.py b/esphome/components/libretiny/__init__.py index 4c8a1999f9..4fbbcde6c3 100644 --- a/esphome/components/libretiny/__init__.py +++ b/esphome/components/libretiny/__init__.py @@ -32,6 +32,7 @@ from .const import ( CONF_SDK_SILENT, CONF_UART_PORT, FAMILIES, + FAMILY_BK7231N, FAMILY_COMPONENT, FAMILY_FRIENDLY, KEY_BOARD, @@ -50,6 +51,22 @@ CODEOWNERS = ["@kuba2k2"] AUTO_LOAD = ["preferences"] IS_TARGET_PLATFORM = True +# BK7231N SDK options to disable unused features. +# Disabling BLE saves ~21KB RAM and ~200KB Flash because BLE init code is +# called unconditionally by the SDK. ESPHome doesn't use BLE on LibreTiny. +# +# This only works on BK7231N (BLE 5.x). Other BK72XX chips using BLE 4.2 +# (BK7231T, BK7231Q, BK7251; BK7252 boards use the BK7251 family) have a bug +# where the BLE library still links and references undefined symbols when +# CFG_SUPPORT_BLE=0. +# +# Other options like CFG_TX_EVM_TEST, CFG_RX_SENSITIVITY_TEST, CFG_SUPPORT_BKREG, +# CFG_SUPPORT_OTA_HTTP, and CFG_USE_SPI_SLAVE were evaluated but provide no # NOLINT +# measurable benefit - the linker already strips unreferenced code via -gc-sections. +_BK7231N_SYS_CONFIG_OPTIONS = [ + "CFG_SUPPORT_BLE=0", +] + def _detect_variant(value): if KEY_LIBRETINY not in CORE.data: @@ -346,4 +363,10 @@ async def component_to_code(config): cg.add_platformio_option("custom_fw_name", "esphome") cg.add_platformio_option("custom_fw_version", __version__) + # Apply chip-specific SDK options to save RAM/Flash + if config[CONF_FAMILY] == FAMILY_BK7231N: + cg.add_platformio_option( + "custom_options.sys_config#h", _BK7231N_SYS_CONFIG_OPTIONS + ) + await cg.register_component(var, config) From a1395af7636e787dda0e1e916d8afc4fbcd2b046 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 10 Jan 2026 17:07:21 -1000 Subject: [PATCH 6/6] [helpers] Add format_hex_prefixed_to for "0x" prefixed hex formatting (#13115) --- esphome/components/one_wire/one_wire.cpp | 6 +++-- .../sml/text_sensor/sml_text_sensor.cpp | 6 ++--- esphome/core/helpers.h | 23 +++++++++++++++++++ 3 files changed, 29 insertions(+), 6 deletions(-) diff --git a/esphome/components/one_wire/one_wire.cpp b/esphome/components/one_wire/one_wire.cpp index fd139d0ddc..187f559ca6 100644 --- a/esphome/components/one_wire/one_wire.cpp +++ b/esphome/components/one_wire/one_wire.cpp @@ -6,8 +6,10 @@ namespace one_wire { static const char *const TAG = "one_wire"; const std::string &OneWireDevice::get_address_name() { - if (this->address_name_.empty()) - this->address_name_ = std::string("0x") + format_hex(this->address_); + if (this->address_name_.empty()) { + char hex_buf[19]; // "0x" + 16 hex chars + null + this->address_name_ = format_hex_prefixed_to(hex_buf, this->address_); + } return this->address_name_; } diff --git a/esphome/components/sml/text_sensor/sml_text_sensor.cpp b/esphome/components/sml/text_sensor/sml_text_sensor.cpp index 6ceff26fe5..17b93ecccf 100644 --- a/esphome/components/sml/text_sensor/sml_text_sensor.cpp +++ b/esphome/components/sml/text_sensor/sml_text_sensor.cpp @@ -24,11 +24,9 @@ void SmlTextSensor::publish_val(const ObisInfo &obis_info) { case SML_HEX: { // Buffer for "0x" + up to 32 bytes as hex + null char buf[67]; - buf[0] = '0'; - buf[1] = 'x'; - // Max 32 bytes of data fit in remaining buffer ((65-1)/2) + // Max 32 bytes of data fit in buffer ((67-3)/2) size_t hex_bytes = std::min(obis_info.value.size(), size_t(32)); - format_hex_to(buf + 2, sizeof(buf) - 2, obis_info.value.begin(), hex_bytes); + format_hex_prefixed_to(buf, obis_info.value.begin(), hex_bytes); publish_state(buf, 2 + hex_bytes * 2); break; } diff --git a/esphome/core/helpers.h b/esphome/core/helpers.h index 05d2d475c1..cd43709f7d 100644 --- a/esphome/core/helpers.h +++ b/esphome/core/helpers.h @@ -759,6 +759,29 @@ inline char *format_hex_to(char (&buffer)[N], T val) { /// Calculate buffer size needed for format_hex_to: "XXXXXXXX...\0" = bytes * 2 + 1 constexpr size_t format_hex_size(size_t byte_count) { return byte_count * 2 + 1; } +/// Calculate buffer size needed for format_hex_prefixed_to: "0xXXXXXXXX...\0" = bytes * 2 + 3 +constexpr size_t format_hex_prefixed_size(size_t byte_count) { return byte_count * 2 + 3; } + +/// Format an unsigned integer as "0x" prefixed lowercase hex to buffer. +template::value, int> = 0> +inline char *format_hex_prefixed_to(char (&buffer)[N], T val) { + static_assert(N >= sizeof(T) * 2 + 3, "Buffer too small for prefixed hex"); + buffer[0] = '0'; + buffer[1] = 'x'; + val = convert_big_endian(val); + format_hex_to(buffer + 2, N - 2, reinterpret_cast(&val), sizeof(T)); + return buffer; +} + +/// Format byte array as "0x" prefixed lowercase hex to buffer. +template inline char *format_hex_prefixed_to(char (&buffer)[N], const uint8_t *data, size_t length) { + static_assert(N >= 5, "Buffer must hold at least '0x' + one hex byte + null"); + buffer[0] = '0'; + buffer[1] = 'x'; + format_hex_to(buffer + 2, N - 2, data, length); + return buffer; +} + /// Calculate buffer size needed for format_hex_pretty_to with separator: "XX:XX:...:XX\0" constexpr size_t format_hex_pretty_size(size_t byte_count) { return byte_count * 3; }